mirror of
https://github.com/Qortal/qortal.git
synced 2025-02-11 17:55:50 +00:00
Basic block and genesis transaction processing + some refactoring.
DB.rebuild() to shutdown and delete database, then rebuild it (schema only). DB.callIdentity() to fetch value of IDENTITY column after an INSERT. Added basic Asset class. Added TODOs to Account. BULK REFACTOR to rename "generation target" back to "generating balance" and "generation signature" back to "generator signature" to ease compatibility with old QORA for now. (Maybe change again in the future if we change from PoS). Added support for Block's totalFees which is either loaded from DB or recalculated as transactions are added to the block. Also in Block: * We can't assume generator's public key is the correct length in case we encounter the GenesisAccount's special 8-byte public key. Fix applied to Block's ResultSet-based constructor. * Forgot to save "signature" column! * Initial version of Block.process() * Block constructor takes transactionsSignature too now Added BlockChain startup/init/validation method to determine whether to (re)build blockchain. Rebuilding blockchain involves rebuilding DB schema, processing GenesisBlock and adding QORA asset. Added some initial GenesisTranaction.process() code: GenesisTransactions are saved but recipient's balance/reference not yet updated. Fix incorrect placeholder bind value for "creation" timestamp in Transaction.save(). Moved incremental database schema updates out from "updates" unit test into DatabaseUpdates class. All unit tests work at this point!
This commit is contained in:
parent
c8167248fc
commit
63be6b7e90
@ -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.
|
||||
* <p>
|
||||
* The connection URL <b>must</b> 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.
|
||||
* <p>
|
||||
* Typical example:
|
||||
* <p>
|
||||
* {@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.
|
||||
* <p>
|
||||
* 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.
|
||||
* <p>
|
||||
* After this method returns, the database <i>can</i> 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.
|
||||
* <p>
|
||||
* See {@link DB#close()} for warnings about connections.
|
||||
* <p>
|
||||
* 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.
|
||||
* <p>
|
||||
* Performs "CALL IDENTITY()" SQL statement to retrieve last value used when INSERTing into a table that has an IDENTITY column.
|
||||
* <p>
|
||||
* 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);
|
||||
}
|
||||
|
||||
}
|
||||
|
294
src/database/DatabaseUpdates.java
Normal file
294
src/database/DatabaseUpdates.java
Normal file
@ -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;
|
||||
}
|
||||
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
49
src/qora/assets/Asset.java
Normal file
49
src/qora/assets/Asset.java
Normal file
@ -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);
|
||||
}
|
||||
}
|
@ -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<Transaction>();
|
||||
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<Transaction> 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
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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;
|
||||
|
@ -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<Transaction>();
|
||||
|
||||
// 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.
|
||||
* <p>
|
||||
* This is handled differently as there is no private key for the genesis account and so no way to sign data.
|
||||
* <p>
|
||||
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
|
||||
}
|
||||
|
16
src/test/blockchain.java
Normal file
16
src/test/blockchain.java
Normal file
@ -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();
|
||||
}
|
||||
|
||||
}
|
@ -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
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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()));
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user