From b90a4860390d75cb75cd30a73c431420de792bcc Mon Sep 17 00:00:00 2001 From: catbref Date: Thu, 17 May 2018 17:39:55 +0100 Subject: [PATCH] More work on Blocks, refactor to using public key in DB, etc. Added brokenmd160.java as command-line support for producing broken MD160 digests. Transactions, and sub-classes, now use/store public key instead of Qora address. (Qora address can be derived from public key and they take up about the same space in DB). Loads more JavaDoc for lovely mouseover help in Eclipse IDE. Crypto.verify() and Crypto.sign() moved into PublicKeyAccount and PrivateKeyAccount as appropriate. Fleshed out Block, added BlockTransactions support. Added TODO comments as Eclipse helpfully lists these for later implementation. Made loading-from-DB-constructors protected/private and also throw NoDataFoundException if unable to load from DB. Public methods can call respective constructors, catch the above exception and return null if they like. Load-from-DB-constructors are to allow sub-classes to load some data from sub-tables and super-class to load from another table. (See PaymentTransaction/Transaction for example). Using public methods allows similar argument lists but with different names, e.g. DBObject.fromSignature(Connection, byte[]) and DBObject.fromReference(Connection, byte[]) Saving into DB maybe still a bit untidy. Looking for a way to close-couple column names with place-holder bind Objects. Less of: connection.prepareStatement("INSERT INTO table (column) VALUES (?)") DB.bindInsertPlaceholders(PreparedStatement, Object...); More like: DB.insertUpdate(String tableName, SomeMagicCloseCoupledPairs...) called like: DB.insertUpdate("Cats", {"name", "Tiddles"}, {"age", 3}); --- src/brokenmd160.java | 21 +++ src/database/DB.java | 59 ++++++- src/database/NoDataFoundException.java | 4 + src/qora/account/Account.java | 8 - src/qora/account/PrivateKeyAccount.java | 11 +- src/qora/account/PublicKeyAccount.java | 9 + src/qora/block/Block.java | 175 +++++++++++++++++-- src/qora/block/BlockTransaction.java | 150 ++++++++++++++++ src/qora/crypto/BrokenMD160.java | 4 +- src/qora/crypto/Crypto.java | 43 +---- src/qora/transaction/PaymentTransaction.java | 44 ++++- src/qora/transaction/Transaction.java | 84 ++++++--- src/qora/transaction/TransactionFactory.java | 18 +- src/settings/Settings.java | 2 +- src/test/crypto.java | 2 +- src/test/load.java | 40 ++--- src/test/migrate.java | 96 ++++++---- src/test/navigation.java | 54 ++++++ src/test/save.java | 4 +- src/test/updates.java | 51 +++--- 20 files changed, 698 insertions(+), 181 deletions(-) create mode 100644 src/brokenmd160.java create mode 100644 src/qora/block/BlockTransaction.java create mode 100644 src/test/navigation.java diff --git a/src/brokenmd160.java b/src/brokenmd160.java new file mode 100644 index 00000000..d5062fb6 --- /dev/null +++ b/src/brokenmd160.java @@ -0,0 +1,21 @@ +import com.google.common.hash.HashCode; + +import qora.crypto.BrokenMD160; + +@SuppressWarnings("deprecation") +public class brokenmd160 { + + public static void main(String args[]) { + if (args.length == 0) { + System.err.println("usage: broken-md160 \noutputs: hex"); + System.exit(1); + } + + byte[] raw = HashCode.fromString(args[0]).asBytes(); + BrokenMD160 brokenMD160 = new BrokenMD160(); + byte[] digest = brokenMD160.digest(raw); + + System.out.println(HashCode.fromBytes(digest).toString()); + } + +} diff --git a/src/database/DB.java b/src/database/DB.java index 0e4c3b0c..dd8366d5 100644 --- a/src/database/DB.java +++ b/src/database/DB.java @@ -26,6 +26,13 @@ public class DB { c.prepareStatement("ROLLBACK").execute(); } + /** + * Convert InputStream, from ResultSet.getBinaryStream(), into byte[] of set length. + * + * @param inputStream + * @param length + * @return byte[length] + */ public static byte[] getResultSetBytes(InputStream inputStream, int length) { if (inputStream == null) return null; @@ -42,6 +49,12 @@ public class DB { return null; } + /** + * Convert InputStream, from ResultSet.getBinaryStream(), into byte[] of unknown length. + * + * @param inputStream + * @return byte[] + */ public static byte[] getResultSetBytes(InputStream inputStream) { final int BYTE_BUFFER_LENGTH = 1024; @@ -64,6 +77,19 @@ public class DB { return result; } + /** + * Format table and column names into an INSERT INTO ... SQL statement. + *

+ * Full form is: + *

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

+ * Note that HSQLDB needs to put into mySQL compatibility mode first via "SET DATABASE SQL SYNTAX MYS TRUE". + * + * @param table + * @param columns + * @return String + */ public static String formatInsertWithPlaceholders(String table, String... columns) { String[] placeholders = new String[columns.length]; Arrays.setAll(placeholders, (int i) -> "?"); @@ -81,9 +107,18 @@ public class DB { return output.toString(); } + /** + * Binds Objects to PreparedStatement based on INSERT INTO ... ON DUPLICATE KEY UPDATE ... + *

+ * Note that each object is bound to two place-holders based on this SQL syntax: + *

+ * INSERT INTO table (column, ...) VALUES (?, ...) ON DUPLICATE KEY UPDATE column=?, ... + * + * @param preparedStatement + * @param objects + * @throws SQLException + */ public static void bindInsertPlaceholders(PreparedStatement preparedStatement, Object... objects) throws SQLException { - // We need to bind two sets of placeholders based on this syntax: - // INSERT INTO table (column, ... ) VALUES (?, ...) ON DUPLICATE KEY UPDATE SET column=?, ... for (int i = 0; i < objects.length; ++i) { Object object = objects[i]; @@ -126,4 +161,24 @@ public class DB { return rs; } + /** + * Execute PreparedStatement and return ResultSet with but added checking + * + * @param preparedStatement + * @return ResultSet, or null if there are no found rows + * @throws SQLException + */ + public static ResultSet checkedExecute(PreparedStatement preparedStatement) throws SQLException { + if (!preparedStatement.execute()) + throw new SQLException("Fetching from database produced no results"); + + ResultSet resultSet = preparedStatement.getResultSet(); + if (resultSet == null) + throw new SQLException("Fetching results from database produced no ResultSet"); + + if (!resultSet.next()) + return null; + + return resultSet; + } } diff --git a/src/database/NoDataFoundException.java b/src/database/NoDataFoundException.java index d63bec42..a9380eeb 100644 --- a/src/database/NoDataFoundException.java +++ b/src/database/NoDataFoundException.java @@ -2,6 +2,10 @@ package database; import java.sql.SQLException; +/** + * Exception for use in DB-backed constructors to indicate no matching data found. + * + */ @SuppressWarnings("serial") public class NoDataFoundException extends SQLException { diff --git a/src/qora/account/Account.java b/src/qora/account/Account.java index 0c74d182..69d81618 100644 --- a/src/qora/account/Account.java +++ b/src/qora/account/Account.java @@ -17,14 +17,6 @@ public class Account { return address; } - // TOSTRING - - @Override - public int hashCode() { - return this.getAddress().hashCode(); - } - - // EQUALS @Override public boolean equals(Object b) { if (!(b instanceof Account)) diff --git a/src/qora/account/PrivateKeyAccount.java b/src/qora/account/PrivateKeyAccount.java index 96b3c220..7cbc3c6e 100644 --- a/src/qora/account/PrivateKeyAccount.java +++ b/src/qora/account/PrivateKeyAccount.java @@ -1,6 +1,7 @@ package qora.account; import qora.crypto.Crypto; +import qora.crypto.Ed25519; import utils.Pair; public class PrivateKeyAccount extends PublicKeyAccount { @@ -10,7 +11,7 @@ public class PrivateKeyAccount extends PublicKeyAccount { public PrivateKeyAccount(byte[] seed) { this.seed = seed; - this.keyPair = Crypto.createKeyPair(seed); + this.keyPair = Ed25519.createKeyPair(seed); this.publicKey = keyPair.getB(); this.address = Crypto.toAddress(this.publicKey); } @@ -27,4 +28,12 @@ public class PrivateKeyAccount extends PublicKeyAccount { return this.keyPair; } + public byte[] sign(byte[] message) { + try { + return Ed25519.sign(this.keyPair, message); + } catch (Exception e) { + return null; + } + } + } diff --git a/src/qora/account/PublicKeyAccount.java b/src/qora/account/PublicKeyAccount.java index 2de9599a..166787fb 100644 --- a/src/qora/account/PublicKeyAccount.java +++ b/src/qora/account/PublicKeyAccount.java @@ -1,6 +1,7 @@ package qora.account; import qora.crypto.Crypto; +import qora.crypto.Ed25519; public class PublicKeyAccount extends Account { @@ -18,4 +19,12 @@ public class PublicKeyAccount extends Account { return publicKey; } + public boolean verify(byte[] signature, byte[] message) { + try { + return Ed25519.verify(signature, message, this.publicKey); + } catch (Exception e) { + return false; + } + } + } diff --git a/src/qora/block/Block.java b/src/qora/block/Block.java index 4a5a0746..c5bab53e 100644 --- a/src/qora/block/Block.java +++ b/src/qora/block/Block.java @@ -1,20 +1,24 @@ package qora.block; +import java.io.ByteArrayInputStream; import java.math.BigDecimal; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; import org.json.simple.JSONObject; import com.google.common.primitives.Bytes; import database.DB; +import database.NoDataFoundException; import qora.account.PrivateKeyAccount; import qora.account.PublicKeyAccount; -import qora.crypto.Crypto; import qora.transaction.Transaction; +import qora.transaction.TransactionFactory; /* * Typical use-case scenarios: @@ -43,7 +47,11 @@ public class Block { // Validation results public static final int VALIDATE_OK = 1; - // Database properties shared with all block types + // 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"; + + // Database properties protected int version; protected byte[] reference; protected int transactionCount; @@ -52,11 +60,14 @@ public class Block { protected int height; protected long timestamp; protected BigDecimal generationTarget; - protected String generator; + protected PublicKeyAccount generator; protected byte[] generationSignature; protected byte[] atBytes; protected BigDecimal atFees; + // Other properties + protected List transactions; + // Property lengths for serialisation protected static final int VERSION_LENGTH = 4; protected static final int REFERENCE_LENGTH = 64; @@ -70,24 +81,27 @@ public class Block { + TRANSACTIONS_SIGNATURE_LENGTH + GENERATION_SIGNATURE_LENGTH + TRANSACTION_COUNT_LENGTH; // Other length constants + protected static final int BLOCK_SIGNATURE_LENGTH = GENERATION_SIGNATURE_LENGTH + TRANSACTIONS_SIGNATURE_LENGTH; public static final int MAX_BLOCK_BYTES = 1048576; - protected static final int TRANSACTION_SIZE_LENGTH = 4; - public static final int MAX_TRANSACTION_BYTES = MAX_BLOCK_BYTES - BASE_LENGTH - TRANSACTION_SIZE_LENGTH; + protected static final int TRANSACTION_SIZE_LENGTH = 4; // per transaction + public static final int MAX_TRANSACTION_BYTES = MAX_BLOCK_BYTES - BASE_LENGTH; protected static final int AT_BYTES_LENGTH = 4; protected static final int AT_FEES_LENGTH = 8; protected static final int AT_LENGTH = AT_FEES_LENGTH + AT_BYTES_LENGTH; // Constructors - protected Block(int version, byte[] reference, long timestamp, BigDecimal generationTarget, String generator, byte[] generationSignature, byte[] atBytes, - BigDecimal atFees) { + protected Block(int version, byte[] reference, long timestamp, BigDecimal generationTarget, PublicKeyAccount generator, byte[] generationSignature, + byte[] atBytes, BigDecimal atFees) { this.version = version; this.reference = reference; this.timestamp = timestamp; this.generationTarget = generationTarget; this.generator = generator; this.generationSignature = generationSignature; + this.height = 0; this.transactionCount = 0; + this.transactions = null; this.transactionsSignature = null; this.totalFees = null; @@ -113,7 +127,7 @@ public class Block { return this.generationTarget; } - public String getGenerator() { + public PublicKeyAccount getGenerator() { return this.generator; } @@ -141,6 +155,10 @@ public class Block { return this.atFees; } + public int getHeight() { + return this.height; + } + // More information public byte[] getSignature() { @@ -151,17 +169,59 @@ public class Block { } public int getDataLength() { - return 0; + int blockLength = BASE_LENGTH; + + if (version >= 2 && this.atBytes != null) + blockLength += AT_FEES_LENGTH + AT_BYTES_LENGTH + this.atBytes.length; + + // Short cut for no transactions + if (this.transactions == null || this.transactions.isEmpty()) + return blockLength; + + for (Transaction transaction : this.transactions) + blockLength += TRANSACTION_SIZE_LENGTH + transaction.getDataLength(); + + return blockLength; + } + + public List getTransactions() { + return this.transactions; + } + + public List getTransactions(Connection connection) throws SQLException { + // Already loaded? + if (this.transactions != null) + return this.transactions; + + // Load from DB + this.transactions = new ArrayList(); + + PreparedStatement preparedStatement = connection.prepareStatement("SELECT transaction_signature FROM BlockTransactions WHERE block_signature = ?"); + preparedStatement.setBinaryStream(1, new ByteArrayInputStream(this.getSignature())); + if (!preparedStatement.execute()) + throw new SQLException("Fetching from database produced no results"); + + ResultSet rs = preparedStatement.getResultSet(); + if (rs == null) + throw new SQLException("Fetching results from database produced no ResultSet"); + + while (rs.next()) { + byte[] transactionSignature = DB.getResultSetBytes(rs.getBinaryStream(1), Transaction.SIGNATURE_LENGTH); + this.transactions.add(TransactionFactory.fromSignature(connection, transactionSignature)); + } + + return this.transactions; } // Load/Save protected Block(Connection connection, byte[] signature) throws SQLException { - ResultSet rs = DB.executeUsingBytes(connection, - "SELECT version, reference, transaction_count, total_fees, " - + "transactions_signature, height, generation, generation_target, generator, generation_signature, " - + "AT_data, AT_fees FROM Blocks WHERE signature = ?", - signature); + this(DB.executeUsingBytes(connection, "SELECT " + DB_COLUMNS + " FROM Blocks WHERE signature = ?", signature)); + } + + protected Block(ResultSet rs) throws SQLException { + if (rs == null) + throw new NoDataFoundException(); this.version = rs.getInt(1); this.reference = DB.getResultSetBytes(rs.getBinaryStream(2), REFERENCE_LENGTH); @@ -171,18 +231,53 @@ public class Block { this.height = rs.getInt(6); this.timestamp = rs.getTimestamp(7).getTime(); this.generationTarget = rs.getBigDecimal(8); - this.generator = rs.getString(9); + this.generator = new PublicKeyAccount(DB.getResultSetBytes(rs.getBinaryStream(9), GENERATOR_LENGTH)); this.generationSignature = DB.getResultSetBytes(rs.getBinaryStream(10), GENERATION_SIGNATURE_LENGTH); this.atBytes = DB.getResultSetBytes(rs.getBinaryStream(11)); this.atFees = rs.getBigDecimal(12); } + /** + * Load Block from DB using block signature. + * + * @param connection + * @param signature + * @return Block, or null if not found + * @throws SQLException + */ + public static Block fromSignature(Connection connection, byte[] signature) throws SQLException { + try { + return new Block(connection, signature); + } catch (NoDataFoundException e) { + return null; + } + } + + /** + * Load Block from DB using block height + * + * @param connection + * @param height + * @return Block, or null if not found + * @throws SQLException + */ + public static Block fromHeight(Connection connection, int height) throws SQLException { + PreparedStatement preparedStatement = connection.prepareStatement("SELECT signature FROM Blocks WHERE height = ?"); + preparedStatement.setInt(1, height); + + try { + return new Block(DB.checkedExecute(preparedStatement)); + } catch (NoDataFoundException e) { + return null; + } + } + 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"); 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, this.generationSignature, this.atBytes, this.atFees); + this.height, this.timestamp, this.generationTarget, this.generator.getPublicKey(), this.generationSignature, this.atBytes, this.atFees); preparedStatement.execute(); // Save transactions @@ -191,19 +286,58 @@ public class Block { // Navigation + /** + * Load parent Block from DB + * + * @param connection + * @return Block, or null if not found + * @throws SQLException + */ + public Block getParent(Connection connection) throws SQLException { + try { + return new Block(connection, this.reference); + } catch (NoDataFoundException e) { + return null; + } + } + + /** + * Load child Block from DB + * + * @param connection + * @return Block, or null if not found + * @throws SQLException + */ + public Block getChild(Connection connection) throws SQLException { + byte[] blockSignature = this.getSignature(); + if (blockSignature == null) + return null; + + ResultSet resultSet = DB.executeUsingBytes(connection, "SELECT " + DB_COLUMNS + " FROM Blocks WHERE reference = ?", blockSignature); + + try { + return new Block(resultSet); + } catch (NoDataFoundException e) { + return null; + } + } + // Converters public JSONObject toJSON() { + // TODO return null; } public byte[] toBytes() { + // TODO return null; } // Processing public boolean addTransaction(Transaction transaction) { + // TODO // Check there is space in block // Add to block // Update transaction count @@ -212,23 +346,26 @@ public class Block { } public byte[] calcSignature(PrivateKeyAccount signer) { - byte[] bytes = this.toBytes(); - - return Crypto.sign(signer, bytes); + // TODO + return null; } public boolean isSignatureValid(PublicKeyAccount signer) { + // TODO return false; } public int isValid() { + // TODO return VALIDATE_OK; } public void process() { + // TODO } public void orphan() { + // TODO } } diff --git a/src/qora/block/BlockTransaction.java b/src/qora/block/BlockTransaction.java new file mode 100644 index 00000000..f632ceab --- /dev/null +++ b/src/qora/block/BlockTransaction.java @@ -0,0 +1,150 @@ +package qora.block; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; + +import java.io.ByteArrayInputStream; + +import org.json.simple.JSONObject; + +import database.DB; +import database.NoDataFoundException; +import qora.transaction.Transaction; +import qora.transaction.TransactionFactory; + +public class BlockTransaction { + + // Database properties shared with all transaction types + protected byte[] blockSignature; + protected int sequence; + protected byte[] transactionSignature; + + // Constructors + + protected BlockTransaction(byte[] blockSignature, int sequence, byte[] transactionSignature) { + this.blockSignature = blockSignature; + this.sequence = sequence; + this.transactionSignature = transactionSignature; + } + + // Getters/setters + + public byte[] getBlockSignature() { + return this.blockSignature; + } + + public int getSequence() { + return this.sequence; + } + + public byte[] getTransactionSignature() { + return this.transactionSignature; + } + + // More information + + // Load/Save + + protected BlockTransaction(Connection connection, byte[] blockSignature, int sequence) throws SQLException { + // Can't use DB.executeUsingBytes() here as we need two placeholders + PreparedStatement preparedStatement = connection + .prepareStatement("SELECT transaction_signature FROM BlockTransactions WHERE block_signature = ? and sequence = ?"); + preparedStatement.setBinaryStream(1, new ByteArrayInputStream(blockSignature)); + preparedStatement.setInt(2, sequence); + + ResultSet rs = DB.checkedExecute(preparedStatement); + if (rs == null) + throw new NoDataFoundException(); + + this.blockSignature = blockSignature; + this.sequence = sequence; + this.transactionSignature = DB.getResultSetBytes(rs.getBinaryStream(1), Transaction.SIGNATURE_LENGTH); + } + + protected BlockTransaction(Connection connection, byte[] transactionSignature) throws SQLException { + ResultSet rs = DB.executeUsingBytes(connection, "SELECT block_signature, sequence FROM BlockTransactions WHERE transaction_signature = ?", + transactionSignature); + if (rs == null) + throw new NoDataFoundException(); + + this.blockSignature = DB.getResultSetBytes(rs.getBinaryStream(1), Block.BLOCK_SIGNATURE_LENGTH); + this.sequence = rs.getInt(2); + this.transactionSignature = transactionSignature; + } + + /** + * Load BlockTransaction from DB using block signature and tx-in-block sequence. + * + * @param connection + * @param blockSignature + * @param sequence + * @return BlockTransaction, or null if not found + * @throws SQLException + */ + public static BlockTransaction fromBlockSignature(Connection connection, byte[] blockSignature, int sequence) throws SQLException { + try { + return new BlockTransaction(connection, blockSignature, sequence); + } catch (NoDataFoundException e) { + return null; + } + } + + /** + * Load BlockTransaction from DB using transaction signature. + * + * @param connection + * @param transactionSignature + * @return BlockTransaction, or null if not found + * @throws SQLException + */ + public static BlockTransaction fromTransactionSignature(Connection connection, byte[] transactionSignature) throws SQLException { + try { + return new BlockTransaction(connection, transactionSignature); + } catch (NoDataFoundException e) { + return null; + } + } + + protected void save(Connection connection) throws SQLException { + String sql = DB.formatInsertWithPlaceholders("BlockTransactions", "block_signature", "sequence", "transaction_signature"); + PreparedStatement preparedStatement = connection.prepareStatement(sql); + DB.bindInsertPlaceholders(preparedStatement, this.blockSignature, this.sequence, this.transactionSignature); + preparedStatement.execute(); + } + + // Navigation + + /** + * Load corresponding Block from DB. + * + * @param connection + * @return Block, or null if not found (which should never happen) + * @throws SQLException + */ + public Block getBlock(Connection connection) throws SQLException { + return Block.fromSignature(connection, this.blockSignature); + } + + /** + * Load corresponding Transaction from DB. + * + * @param connection + * @return Transaction, or null if not found (which should never happen) + * @throws SQLException + */ + public Transaction getTransaction(Connection connection) throws SQLException { + return TransactionFactory.fromSignature(connection, this.transactionSignature); + } + + // Converters + + public JSONObject toJSON() { + // TODO + return null; + } + + // Processing + +} diff --git a/src/qora/crypto/BrokenMD160.java b/src/qora/crypto/BrokenMD160.java index bea0b2ca..d42b8a34 100644 --- a/src/qora/crypto/BrokenMD160.java +++ b/src/qora/crypto/BrokenMD160.java @@ -1,9 +1,9 @@ package qora.crypto; /** - * BROKEN RIPEMD160 + * BROKEN RIPEMD160 *

- * DO NOT USE in future code as this implementation is BROKEN and returns incorrect digests for some inputs. + * DO NOT USE in future code as this implementation is BROKEN and returns incorrect digests for some inputs. *

* It is only "grand-fathered" here to produce correct QORA addresses. */ diff --git a/src/qora/crypto/Crypto.java b/src/qora/crypto/Crypto.java index e5dcc77e..74558e96 100644 --- a/src/qora/crypto/Crypto.java +++ b/src/qora/crypto/Crypto.java @@ -4,18 +4,11 @@ import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.Arrays; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; - import qora.account.Account; -import qora.account.PrivateKeyAccount; import utils.Base58; -import utils.Pair; public class Crypto { - private static final Logger LOGGER = LogManager.getLogger(Crypto.class); - public static final byte ADDRESS_VERSION = 58; public static final byte AT_ADDRESS_VERSION = 23; @@ -34,16 +27,6 @@ public class Crypto { return digest(digest(input)); } - public static Pair createKeyPair(byte[] seed) { - try { - // Generate private and public key pair - return Ed25519.createKeyPair(seed); - } catch (Exception e) { - LOGGER.error(e.getMessage(), e); - return null; - } - } - @SuppressWarnings("deprecation") private static String toAddress(byte addressVersion, byte[] input) { // SHA2-256 input to create new data and of known size @@ -80,7 +63,7 @@ public class Crypto { public static boolean isValidAddress(String address) { byte[] addressBytes; - + try { // Attempt Base58 decoding addressBytes = Base58.decode(address); @@ -98,33 +81,13 @@ public class Crypto { case AT_ADDRESS_VERSION: byte[] addressWithoutChecksum = Arrays.copyOf(addressBytes, addressBytes.length - 4); byte[] passedChecksum = Arrays.copyOfRange(addressWithoutChecksum, addressBytes.length - 4, addressBytes.length); - + byte[] generatedChecksum = doubleDigest(addressWithoutChecksum); return Arrays.equals(passedChecksum, generatedChecksum); - + default: return false; } } - public static byte[] sign(PrivateKeyAccount account, byte[] message) { - try { - // GET SIGNATURE - return Ed25519.sign(account.getKeyPair(), message); - } catch (Exception e) { - LOGGER.error(e.getMessage(),e); - return new byte[64]; - } - } - - public static boolean verify(byte[] publicKey, byte[] signature, byte[] message) { - try { - // VERIFY SIGNATURE - return Ed25519.verify(signature, message, publicKey); - } catch (Exception e) { - LOGGER.error(e.getMessage(),e); - return false; - } - } - } diff --git a/src/qora/transaction/PaymentTransaction.java b/src/qora/transaction/PaymentTransaction.java index 573114e6..447cef0e 100644 --- a/src/qora/transaction/PaymentTransaction.java +++ b/src/qora/transaction/PaymentTransaction.java @@ -10,12 +10,12 @@ import org.json.simple.JSONObject; import database.DB; import database.NoDataFoundException; +import qora.account.PublicKeyAccount; public class PaymentTransaction extends Transaction { // Properties - // private PublicKeyAccount sender; - private String sender; + private PublicKeyAccount sender; private String recipient; private BigDecimal amount; @@ -27,7 +27,8 @@ public class PaymentTransaction extends Transaction { // Constructors - public PaymentTransaction(String sender, String recipient, BigDecimal amount, BigDecimal fee, long timestamp, byte[] reference, byte[] signature) { + public PaymentTransaction(PublicKeyAccount sender, String recipient, BigDecimal amount, BigDecimal fee, long timestamp, byte[] reference, + byte[] signature) { super(TransactionType.Payment, fee, sender, timestamp, reference, signature); this.sender = sender; @@ -35,13 +36,13 @@ public class PaymentTransaction extends Transaction { this.amount = amount; } - public PaymentTransaction(String sender, String recipient, BigDecimal amount, BigDecimal fee, long timestamp, byte[] reference) { + public PaymentTransaction(PublicKeyAccount sender, String recipient, BigDecimal amount, BigDecimal fee, long timestamp, byte[] reference) { this(sender, recipient, amount, fee, timestamp, reference, null); } // Getters/Setters - public String getSender() { + public PublicKeyAccount getSender() { return this.sender; } @@ -61,25 +62,50 @@ public class PaymentTransaction extends Transaction { // Load/Save - public PaymentTransaction(Connection connection, byte[] signature) throws SQLException { + /** + * Load PaymentTransaction from DB using signature. + * + * @param connection + * @param signature + * @throws NoDataFoundException + * if no matching row found + * @throws SQLException + */ + protected PaymentTransaction(Connection connection, byte[] signature) throws SQLException { super(connection, TransactionType.Payment, signature); ResultSet rs = DB.executeUsingBytes(connection, "SELECT sender, recipient, amount FROM PaymentTransactions WHERE signature = ?", signature); if (rs == null) throw new NoDataFoundException(); - this.sender = rs.getString(1); + this.sender = new PublicKeyAccount(DB.getResultSetBytes(rs.getBinaryStream(1), CREATOR_LENGTH)); this.recipient = rs.getString(2); this.amount = rs.getBigDecimal(3).setScale(8); } + /** + * Load PaymentTransaction from DB using signature + * + * @param connection + * @param signature + * @return PaymentTransaction, or null if not found + * @throws SQLException + */ + public static PaymentTransaction fromSignature(Connection connection, byte[] signature) throws SQLException { + try { + return new PaymentTransaction(connection, signature); + } catch (NoDataFoundException e) { + return null; + } + } + @Override public void save(Connection connection) throws SQLException { super.save(connection); String sql = DB.formatInsertWithPlaceholders("PaymentTransactions", "signature", "sender", "recipient", "amount"); PreparedStatement preparedStatement = connection.prepareStatement(sql); - DB.bindInsertPlaceholders(preparedStatement, this.signature, this.sender, this.recipient, this.amount); + DB.bindInsertPlaceholders(preparedStatement, this.signature, this.sender.getPublicKey(), this.recipient, this.amount); preparedStatement.execute(); } @@ -109,9 +135,11 @@ public class PaymentTransaction extends Transaction { } public void process() { + // TODO } public void orphan() { + // TODO } } diff --git a/src/qora/transaction/Transaction.java b/src/qora/transaction/Transaction.java index 768c24b9..d1acd3fd 100644 --- a/src/qora/transaction/Transaction.java +++ b/src/qora/transaction/Transaction.java @@ -18,7 +18,8 @@ import database.DB; import database.NoDataFoundException; import qora.account.PrivateKeyAccount; import qora.account.PublicKeyAccount; -import qora.crypto.Crypto; +import qora.block.Block; +import qora.block.BlockTransaction; import settings.Settings; public abstract class Transaction { @@ -52,26 +53,28 @@ public abstract class Transaction { // Database properties shared with all transaction types protected TransactionType type; - protected String creator; + protected PublicKeyAccount creator; protected long timestamp; protected byte[] reference; protected BigDecimal fee; protected byte[] signature; // Derived/cached properties - // maybe: protected PublicKeyAccount creatorAccount; - - // Property lengths + + // Property lengths for serialisation protected static final int TYPE_LENGTH = 4; protected static final int TIMESTAMP_LENGTH = 8; protected static final int REFERENCE_LENGTH = 64; protected static final int FEE_LENGTH = 8; - protected static final int SIGNATURE_LENGTH = 64; + public static final int SIGNATURE_LENGTH = 64; protected static final int BASE_TYPELESS_LENGTH = TIMESTAMP_LENGTH + REFERENCE_LENGTH + FEE_LENGTH + SIGNATURE_LENGTH; + // Other length constants + protected static final int CREATOR_LENGTH = 32; + // Constructors - protected Transaction(TransactionType type, BigDecimal fee, String creator, long timestamp, byte[] reference, byte[] signature) { + protected Transaction(TransactionType type, BigDecimal fee, PublicKeyAccount creator, long timestamp, byte[] reference, byte[] signature) { this.fee = fee; this.type = type; this.creator = creator; @@ -80,7 +83,7 @@ public abstract class Transaction { this.signature = signature; } - protected Transaction(TransactionType type, BigDecimal fee, String creator, long timestamp, byte[] reference) { + protected Transaction(TransactionType type, BigDecimal fee, PublicKeyAccount creator, long timestamp, byte[] reference) { this(type, fee, creator, timestamp, reference, null); } @@ -90,7 +93,7 @@ public abstract class Transaction { return this.type; } - public String getCreator() { + public PublicKeyAccount getCreator() { return this.creator; } @@ -148,6 +151,20 @@ public abstract class Transaction { // Load/Save + // Typically called by sub-class' load-from-DB constructors + + /** + * Load base Transaction from DB using signature. + *

+ * Note that the transaction type is not checked against the DB's value. + * + * @param connection + * @param type + * @param signature + * @throws NoDataFoundException + * if no matching row found + * @throws SQLException + */ protected Transaction(Connection connection, TransactionType type, byte[] signature) throws SQLException { ResultSet rs = DB.executeUsingBytes(connection, "SELECT reference, creator, creation, fee FROM Transactions WHERE signature = ?", signature); if (rs == null) @@ -155,7 +172,7 @@ public abstract class Transaction { this.type = type; this.reference = DB.getResultSetBytes(rs.getBinaryStream(1), REFERENCE_LENGTH); - this.creator = rs.getString(2); + this.creator = new PublicKeyAccount(DB.getResultSetBytes(rs.getBinaryStream(2), CREATOR_LENGTH)); this.timestamp = rs.getTimestamp(3).getTime(); this.fee = rs.getBigDecimal(4).setScale(8); this.signature = signature; @@ -164,32 +181,57 @@ public abstract class Transaction { protected void save(Connection connection) throws SQLException { 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, + DB.bindInsertPlaceholders(preparedStatement, this.signature, this.reference, this.type.value, this.creator.getPublicKey(), Timestamp.from(Instant.ofEpochSecond(this.timestamp)), this.fee, null); preparedStatement.execute(); } // Navigation - /* - * public Block getBlock() { BlockTransaction blockTx = BlockTransaction.fromTransactionSignature(this.signature); if (blockTx == null) return null; + /** + * Load encapsulating Block from DB, if any * - * return Block.fromSignature(blockTx.getSignature()); } + * @param connection + * @return Block, or null if transaction is not in a Block + * @throws SQLException + */ + public Block getBlock(Connection connection) throws SQLException { + if (this.signature == null) + return null; + + BlockTransaction blockTx = BlockTransaction.fromTransactionSignature(connection, this.signature); + if (blockTx == null) + return null; + + return Block.fromSignature(connection, blockTx.getBlockSignature()); + } + + /** + * Load parent Transaction from DB via this transaction's reference. * + * @param connection + * @return Transaction, or null if no parent found (which should not happen) + * @throws SQLException */ - public Transaction getParent(Connection connection) throws SQLException { if (this.reference == null) return null; - - return TransactionFactory.fromSignature(connection, this.reference); + + return TransactionFactory.fromSignature(connection, this.reference); } + /** + * Load child Transaction from DB, if any. + * + * @param connection + * @return Transaction, or null if no child found + * @throws SQLException + */ public Transaction getChild(Connection connection) throws SQLException { if (this.signature == null) return null; - - return TransactionFactory.fromReference(connection, this.signature); + + return TransactionFactory.fromReference(connection, this.signature); } // Converters @@ -203,14 +245,14 @@ public abstract class Transaction { public byte[] calcSignature(PrivateKeyAccount signer) { byte[] bytes = this.toBytes(); - return Crypto.sign(signer, bytes); + return signer.sign(bytes); } public boolean isSignatureValid(PublicKeyAccount signer) { if (this.signature == null) return false; - return Crypto.verify(signer.getPublicKey(), this.signature, this.toBytes()); + return signer.verify(this.signature, this.toBytes()); } public abstract int isValid(); diff --git a/src/qora/transaction/TransactionFactory.java b/src/qora/transaction/TransactionFactory.java index 3fda28b0..80100586 100644 --- a/src/qora/transaction/TransactionFactory.java +++ b/src/qora/transaction/TransactionFactory.java @@ -9,11 +9,27 @@ import qora.transaction.Transaction.TransactionType; public class TransactionFactory { + /** + * Load Transaction from DB using signature. + * + * @param connection + * @param signature + * @return ? extends Transaction, or null if not found + * @throws SQLException + */ public static Transaction fromSignature(Connection connection, byte[] signature) throws SQLException { ResultSet resultSet = DB.executeUsingBytes(connection, "SELECT type, signature FROM Transactions WHERE signature = ?", signature); return fromResultSet(connection, resultSet); } + /** + * Load Transaction from DB using reference. + * + * @param connection + * @param reference + * @return ? extends Transaction, or null if not found + * @throws SQLException + */ public static Transaction fromReference(Connection connection, byte[] reference) throws SQLException { ResultSet resultSet = DB.executeUsingBytes(connection, "SELECT type, signature FROM Transactions WHERE reference = ?", reference); return fromResultSet(connection, resultSet); @@ -35,7 +51,7 @@ public class TransactionFactory { return null; case Payment: - return new PaymentTransaction(connection, signature); + return PaymentTransaction.fromSignature(connection, signature); default: return null; diff --git a/src/settings/Settings.java b/src/settings/Settings.java index fca4fbc9..8282dc47 100644 --- a/src/settings/Settings.java +++ b/src/settings/Settings.java @@ -5,7 +5,7 @@ public class Settings { public static Settings getInstance() { return new Settings(); } - + public int getMaxBytePerFee() { return 1024; } diff --git a/src/test/crypto.java b/src/test/crypto.java index 4abb7aa3..0897ec3c 100644 --- a/src/test/crypto.java +++ b/src/test/crypto.java @@ -35,5 +35,5 @@ public class crypto { assertEquals(expected, Crypto.toAddress(publicKey)); } - + } diff --git a/src/test/load.java b/src/test/load.java index 33922162..6a56fe54 100644 --- a/src/test/load.java +++ b/src/test/load.java @@ -36,16 +36,17 @@ public class load { public void testLoadPaymentTransaction() throws SQLException { String signature58 = "1211ZPwG3hk5evWzXCZi9hMDRpwumWmkENjwWkeTCik9xA5uoYnxzF7rwR5hmHH3kG2RXo7ToCAaRc7dvnynByJt"; byte[] signature = Base58.decode(signature58); - - PaymentTransaction paymentTransaction = new PaymentTransaction(connection, signature); - assertEquals(paymentTransaction.getSender(), "QXwu8924WdgPoRmtiWQBUMF6eedmp1Hu2E"); - assertEquals(paymentTransaction.getCreator(), "QXwu8924WdgPoRmtiWQBUMF6eedmp1Hu2E"); + PaymentTransaction paymentTransaction = PaymentTransaction.fromSignature(connection, signature); + + assertEquals(paymentTransaction.getSender().getAddress(), "QXwu8924WdgPoRmtiWQBUMF6eedmp1Hu2E"); + assertEquals(paymentTransaction.getCreator().getAddress(), "QXwu8924WdgPoRmtiWQBUMF6eedmp1Hu2E"); assertEquals(paymentTransaction.getRecipient(), "QZsv8vbJ6QfrBNba4LMp5UtHhAzhrxvVUU"); assertEquals(paymentTransaction.getTimestamp(), 1416209264000L); - assertEquals(Base58.encode(paymentTransaction.getReference()), "31dC6kHHBeG5vYb8LMaZDjLEmhc9kQB2VUApVd8xWncSRiXu7yMejdprjYFMP2rUnzZxWd4KJhkq6LsV7rQvU1kY"); + assertEquals(Base58.encode(paymentTransaction.getReference()), + "31dC6kHHBeG5vYb8LMaZDjLEmhc9kQB2VUApVd8xWncSRiXu7yMejdprjYFMP2rUnzZxWd4KJhkq6LsV7rQvU1kY"); } - + @Test public void testLoadFactory() throws SQLException { String signature58 = "1211ZPwG3hk5evWzXCZi9hMDRpwumWmkENjwWkeTCik9xA5uoYnxzF7rwR5hmHH3kG2RXo7ToCAaRc7dvnynByJt"; @@ -55,28 +56,27 @@ public class load { Transaction transaction = TransactionFactory.fromSignature(connection, signature); if (transaction == null) break; - + PaymentTransaction payment = (PaymentTransaction) transaction; - System.out.println("Transaction " + Base58.encode(payment.getSignature()) + ": " + payment.getAmount().toString() + " QORA from " + payment.getSender() + " to " + payment.getRecipient()); - + System.out.println("Transaction " + Base58.encode(payment.getSignature()) + ": " + payment.getAmount().toString() + " QORA from " + + payment.getSender().getAddress() + " to " + payment.getRecipient()); + signature = payment.getReference(); } } - + @Test public void testLoadNonexistentTransaction() throws SQLException { String signature58 = "1111222233334444"; byte[] signature = Base58.decode(signature58); - - try { - PaymentTransaction payment = new PaymentTransaction(connection, signature); - System.out.println("Transaction " + Base58.encode(payment.getSignature()) + ": " + payment.getAmount().toString() + " QORA from " + payment.getSender() + " to " + payment.getRecipient()); - } catch (SQLException e) { - System.out.println(e.getMessage()); - return; - } - fail(); + PaymentTransaction payment = PaymentTransaction.fromSignature(connection, signature); + + if (payment != null) { + System.out.println("Transaction " + Base58.encode(payment.getSignature()) + ": " + payment.getAmount().toString() + " QORA from " + + payment.getSender().getAddress() + " to " + payment.getRecipient()); + fail(); + } } - + } diff --git a/src/test/migrate.java b/src/test/migrate.java index cb27a546..be5017db 100644 --- a/src/test/migrate.java +++ b/src/test/migrate.java @@ -7,7 +7,6 @@ import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; -import java.io.UnsupportedEncodingException; import java.math.BigDecimal; import java.net.URL; import java.nio.charset.Charset; @@ -18,7 +17,9 @@ import java.sql.Statement; import java.sql.Timestamp; import java.util.ArrayList; import java.util.Arrays; +import java.util.HashMap; import java.util.List; +import java.util.Map; import org.json.simple.JSONArray; import org.json.simple.JSONObject; @@ -29,15 +30,21 @@ import org.junit.Before; import org.junit.Test; import com.google.common.hash.HashCode; +import com.google.common.io.CharStreams; import utils.Base58; public class migrate { + private static final String GENESIS_ADDRESS = "QfGMeDQQUQePMpAmfLBJzgqyrM35RWxHGD"; + private static final byte[] GENESIS_PUBLICKEY = new byte[] { 1, 1, 1, 1, 1, 1, 1, 1 }; + private static Connection c; private static PreparedStatement startTransactionPStmt; private static PreparedStatement commitPStmt; + private static Map publicKeyByAddress = new HashMap(); + @Before public void connect() throws SQLException { c = common.getConnection(); @@ -54,8 +61,9 @@ public class migrate { } } - public Object fetchBlockJSON(int height) { + public Object fetchBlockJSON(int height) throws IOException { InputStream is; + try { is = new URL("http://localhost:9085/blocks/byheight/" + height).openStream(); } catch (IOException e) { @@ -65,13 +73,28 @@ public class migrate { try { BufferedReader reader = new BufferedReader(new InputStreamReader(is, Charset.forName("UTF-8"))); return JSONValue.parseWithException(reader); - } catch (IOException | ParseException e) { + } catch (ParseException e) { return null; } finally { - try { - is.close(); - } catch (IOException e) { - } + is.close(); + } + } + + public byte[] addressToPublicKey(String address) throws IOException { + byte[] cachedPublicKey = publicKeyByAddress.get(address); + if (cachedPublicKey != null) + return cachedPublicKey; + + InputStream is = new URL("http://localhost:9085/addresses/publickey/" + address).openStream(); + + try { + String publicKey58 = CharStreams.toString(new InputStreamReader(is, Charset.forName("UTF-8"))); + + byte[] publicKey = Base58.decode(publicKey58); + publicKeyByAddress.put(address, publicKey); + return publicKey; + } finally { + is.close(); } } @@ -97,7 +120,12 @@ public class migrate { } @Test - public void testMigration() throws SQLException, UnsupportedEncodingException { + public void testMigration() throws SQLException, IOException { + // Genesis public key + publicKeyByAddress.put(GENESIS_ADDRESS, GENESIS_PUBLICKEY); + // Some other public keys for addresses that have never created a transaction + publicKeyByAddress.put("QcDLhirHkSbR4TLYeShLzHw61B8UGTFusk", Base58.decode("HP58uWRBae654ze6ysmdyGv3qaDrr9BEk6cHv4WuiF7d")); + Statement stmt = c.createStatement(); stmt.execute("DELETE FROM Blocks"); @@ -148,7 +176,8 @@ public class migrate { PreparedStatement sharedPaymentPStmt = c .prepareStatement("INSERT INTO SharedTransactionPayments " + formatWithPlaceholders("signature", "recipient", "amount", "asset")); - PreparedStatement blockTxPStmt = c.prepareStatement("INSERT INTO BlockTransactions " + formatWithPlaceholders("block", "sequence", "signature")); + PreparedStatement blockTxPStmt = c + .prepareStatement("INSERT INTO BlockTransactions " + formatWithPlaceholders("block_signature", "sequence", "transaction_signature")); int height = 1; byte[] milestone_block = null; @@ -158,7 +187,7 @@ public class migrate { break; if (height % 1000 == 0) - System.out.println("Height: " + height); + System.out.println("Height: " + height + ", public key map size: " + publicKeyByAddress.size()); JSONArray transactions = (JSONArray) json.get("transactions"); @@ -173,6 +202,8 @@ public class migrate { byte[] blockTransactionsSignature = Base58.decode((String) json.get("transactionsSignature")); byte[] blockGeneratorSignature = Base58.decode((String) json.get("generatorSignature")); + byte[] generatorPublicKey = addressToPublicKey((String) json.get("generator")); + blocksPStmt.setBinaryStream(1, new ByteArrayInputStream(blockSignature)); blocksPStmt.setInt(2, ((Long) json.get("version")).intValue()); blocksPStmt.setBinaryStream(3, new ByteArrayInputStream(blockReference)); @@ -182,7 +213,7 @@ public class migrate { blocksPStmt.setInt(7, height); blocksPStmt.setTimestamp(8, new Timestamp((Long) json.get("timestamp"))); blocksPStmt.setBigDecimal(9, BigDecimal.valueOf((Long) json.get("generatingBalance"))); - blocksPStmt.setString(10, (String) json.get("generator")); + blocksPStmt.setBinaryStream(10, new ByteArrayInputStream(generatorPublicKey)); blocksPStmt.setBinaryStream(11, new ByteArrayInputStream(blockGeneratorSignature)); String blockATs = (String) json.get("blockATs"); @@ -221,6 +252,7 @@ public class migrate { txPStmt.setInt(3, type); + // Determine transaction "creator" from specific transaction info switch (type) { case 1: // genesis txPStmt.setNull(4, java.sql.Types.VARCHAR); // genesis transactions only @@ -229,21 +261,21 @@ public class migrate { case 2: // payment case 12: // transfer asset case 15: // multi-payment - txPStmt.setString(4, (String) transaction.get("sender")); + txPStmt.setBinaryStream(4, new ByteArrayInputStream(addressToPublicKey((String) transaction.get("sender")))); break; case 3: // register name - txPStmt.setString(4, (String) transaction.get("registrant")); + txPStmt.setBinaryStream(4, new ByteArrayInputStream(addressToPublicKey((String) transaction.get("registrant")))); break; case 4: // update name case 5: // sell name case 6: // cancel sell name - txPStmt.setString(4, (String) transaction.get("owner")); + txPStmt.setBinaryStream(4, new ByteArrayInputStream(addressToPublicKey((String) transaction.get("owner")))); break; case 7: // buy name - txPStmt.setString(4, (String) transaction.get("buyer")); + txPStmt.setBinaryStream(4, new ByteArrayInputStream(addressToPublicKey((String) transaction.get("buyer")))); break; case 8: // create poll @@ -254,7 +286,7 @@ public class migrate { case 14: // cancel asset order case 16: // deploy CIYAM AT case 17: // message - txPStmt.setString(4, (String) transaction.get("creator")); + txPStmt.setBinaryStream(4, new ByteArrayInputStream(addressToPublicKey((String) transaction.get("creator")))); break; default: @@ -344,7 +376,7 @@ public class migrate { case 2: // payment paymentPStmt.setBinaryStream(1, new ByteArrayInputStream(txSignature)); - paymentPStmt.setString(2, (String) transaction.get("sender")); + paymentPStmt.setBinaryStream(2, new ByteArrayInputStream(addressToPublicKey((String) transaction.get("sender")))); paymentPStmt.setString(3, recipients.get(0)); paymentPStmt.setBigDecimal(4, BigDecimal.valueOf(Double.valueOf((String) transaction.get("amount")).doubleValue())); @@ -354,7 +386,7 @@ public class migrate { case 3: // register name registerNamePStmt.setBinaryStream(1, new ByteArrayInputStream(txSignature)); - registerNamePStmt.setString(2, (String) transaction.get("registrant")); + registerNamePStmt.setBinaryStream(2, new ByteArrayInputStream(addressToPublicKey((String) transaction.get("registrant")))); registerNamePStmt.setString(3, (String) transaction.get("name")); registerNamePStmt.setString(4, (String) transaction.get("owner")); registerNamePStmt.setString(5, (String) transaction.get("value")); @@ -365,7 +397,7 @@ public class migrate { case 4: // update name updateNamePStmt.setBinaryStream(1, new ByteArrayInputStream(txSignature)); - updateNamePStmt.setString(2, (String) transaction.get("owner")); + updateNamePStmt.setBinaryStream(2, new ByteArrayInputStream(addressToPublicKey((String) transaction.get("owner")))); updateNamePStmt.setString(3, (String) transaction.get("name")); updateNamePStmt.setString(4, (String) transaction.get("newOwner")); updateNamePStmt.setString(5, (String) transaction.get("newValue")); @@ -376,7 +408,7 @@ public class migrate { case 5: // sell name sellNamePStmt.setBinaryStream(1, new ByteArrayInputStream(txSignature)); - sellNamePStmt.setString(2, (String) transaction.get("owner")); + sellNamePStmt.setBinaryStream(2, new ByteArrayInputStream(addressToPublicKey((String) transaction.get("owner")))); sellNamePStmt.setString(3, (String) transaction.get("name")); sellNamePStmt.setBigDecimal(4, BigDecimal.valueOf(Double.valueOf((String) transaction.get("amount")).doubleValue())); @@ -386,7 +418,7 @@ public class migrate { case 6: // cancel sell name cancelSellNamePStmt.setBinaryStream(1, new ByteArrayInputStream(txSignature)); - cancelSellNamePStmt.setString(2, (String) transaction.get("owner")); + cancelSellNamePStmt.setBinaryStream(2, new ByteArrayInputStream(addressToPublicKey((String) transaction.get("owner")))); cancelSellNamePStmt.setString(3, (String) transaction.get("name")); cancelSellNamePStmt.execute(); @@ -395,7 +427,7 @@ public class migrate { case 7: // buy name buyNamePStmt.setBinaryStream(1, new ByteArrayInputStream(txSignature)); - buyNamePStmt.setString(2, (String) transaction.get("buyer")); + buyNamePStmt.setBinaryStream(2, new ByteArrayInputStream(addressToPublicKey((String) transaction.get("buyer")))); buyNamePStmt.setString(3, (String) transaction.get("name")); buyNamePStmt.setString(4, (String) transaction.get("seller")); buyNamePStmt.setBigDecimal(5, BigDecimal.valueOf(Double.valueOf((String) transaction.get("amount")).doubleValue())); @@ -406,7 +438,7 @@ public class migrate { case 8: // create poll createPollPStmt.setBinaryStream(1, new ByteArrayInputStream(txSignature)); - createPollPStmt.setString(2, (String) transaction.get("creator")); + createPollPStmt.setBinaryStream(2, new ByteArrayInputStream(addressToPublicKey((String) transaction.get("creator")))); createPollPStmt.setString(3, (String) transaction.get("name")); createPollPStmt.setString(4, (String) transaction.get("description")); @@ -426,7 +458,7 @@ public class migrate { case 9: // vote on poll voteOnPollPStmt.setBinaryStream(1, new ByteArrayInputStream(txSignature)); - voteOnPollPStmt.setString(2, (String) transaction.get("creator")); + voteOnPollPStmt.setBinaryStream(2, new ByteArrayInputStream(addressToPublicKey((String) transaction.get("creator")))); voteOnPollPStmt.setString(3, (String) transaction.get("poll")); voteOnPollPStmt.setInt(4, ((Long) transaction.get("option")).intValue()); @@ -436,7 +468,7 @@ public class migrate { case 10: // arbitrary transactions arbitraryPStmt.setBinaryStream(1, new ByteArrayInputStream(txSignature)); - arbitraryPStmt.setString(2, (String) transaction.get("creator")); + arbitraryPStmt.setBinaryStream(2, new ByteArrayInputStream(addressToPublicKey((String) transaction.get("creator")))); arbitraryPStmt.setInt(3, ((Long) transaction.get("service")).intValue()); arbitraryPStmt.setString(4, "TODO"); @@ -459,7 +491,7 @@ public class migrate { case 11: // issue asset issueAssetPStmt.setBinaryStream(1, new ByteArrayInputStream(txSignature)); - issueAssetPStmt.setString(2, (String) transaction.get("creator")); + issueAssetPStmt.setBinaryStream(2, new ByteArrayInputStream(addressToPublicKey((String) transaction.get("creator")))); issueAssetPStmt.setString(3, (String) transaction.get("name")); issueAssetPStmt.setString(4, (String) transaction.get("description")); issueAssetPStmt.setBigDecimal(5, BigDecimal.valueOf(((Long) transaction.get("quantity")).longValue())); @@ -471,7 +503,7 @@ public class migrate { case 12: // transfer asset transferAssetPStmt.setBinaryStream(1, new ByteArrayInputStream(txSignature)); - transferAssetPStmt.setString(2, (String) transaction.get("sender")); + transferAssetPStmt.setBinaryStream(2, new ByteArrayInputStream(addressToPublicKey((String) transaction.get("sender")))); transferAssetPStmt.setString(3, (String) transaction.get("recipient")); transferAssetPStmt.setLong(4, ((Long) transaction.get("asset")).longValue()); transferAssetPStmt.setBigDecimal(5, BigDecimal.valueOf(Double.valueOf((String) transaction.get("amount")).doubleValue())); @@ -482,7 +514,7 @@ public class migrate { case 13: // create asset order createAssetOrderPStmt.setBinaryStream(1, new ByteArrayInputStream(txSignature)); - createAssetOrderPStmt.setString(2, (String) transaction.get("creator")); + createAssetOrderPStmt.setBinaryStream(2, new ByteArrayInputStream(addressToPublicKey((String) transaction.get("creator")))); JSONObject assetOrder = (JSONObject) transaction.get("order"); createAssetOrderPStmt.setLong(3, ((Long) assetOrder.get("have")).longValue()); @@ -496,7 +528,7 @@ public class migrate { case 14: // cancel asset order cancelAssetOrderPStmt.setBinaryStream(1, new ByteArrayInputStream(txSignature)); - cancelAssetOrderPStmt.setString(2, (String) transaction.get("creator")); + cancelAssetOrderPStmt.setBinaryStream(2, new ByteArrayInputStream(addressToPublicKey((String) transaction.get("creator")))); cancelAssetOrderPStmt.setString(3, (String) transaction.get("order")); cancelAssetOrderPStmt.execute(); @@ -505,7 +537,7 @@ public class migrate { case 15: // multi-payment multiPaymentPStmt.setBinaryStream(1, new ByteArrayInputStream(txSignature)); - multiPaymentPStmt.setString(2, (String) transaction.get("sender")); + multiPaymentPStmt.setBinaryStream(2, new ByteArrayInputStream(addressToPublicKey((String) transaction.get("sender")))); multiPaymentPStmt.execute(); multiPaymentPStmt.clearParameters(); @@ -528,7 +560,7 @@ public class migrate { InputStream creationBytesStream = new ByteArrayInputStream(creationBytes.asBytes()); deployATPStmt.setBinaryStream(1, new ByteArrayInputStream(txSignature)); - deployATPStmt.setString(2, (String) transaction.get("creator")); + deployATPStmt.setBinaryStream(2, new ByteArrayInputStream(addressToPublicKey((String) transaction.get("creator")))); deployATPStmt.setString(3, (String) transaction.get("name")); deployATPStmt.setString(4, (String) transaction.get("description")); deployATPStmt.setString(5, (String) transaction.get("atType")); @@ -554,7 +586,7 @@ public class migrate { } messagePStmt.setBinaryStream(1, new ByteArrayInputStream(txSignature)); - messagePStmt.setString(2, (String) transaction.get("creator")); + messagePStmt.setBinaryStream(2, new ByteArrayInputStream(addressToPublicKey((String) transaction.get("creator")))); messagePStmt.setString(3, (String) transaction.get("recipient")); messagePStmt.setBoolean(4, isText); messagePStmt.setBoolean(5, isEncrypted); diff --git a/src/test/navigation.java b/src/test/navigation.java new file mode 100644 index 00000000..b6bae133 --- /dev/null +++ b/src/test/navigation.java @@ -0,0 +1,54 @@ +package test; + +import static org.junit.Assert.*; + +import java.sql.Connection; +import java.sql.SQLException; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import qora.block.Block; +import qora.transaction.PaymentTransaction; +import utils.Base58; + +public class navigation { + + private static Connection connection; + + @Before + public void connect() throws SQLException { + connection = common.getConnection(); + } + + @After + public void disconnect() { + try { + connection.createStatement().execute("SHUTDOWN"); + } catch (SQLException e) { + fail(); + } + } + + @Test + public void testNavigateFromTransactionToBlock() throws SQLException { + String signature58 = "1211ZPwG3hk5evWzXCZi9hMDRpwumWmkENjwWkeTCik9xA5uoYnxzF7rwR5hmHH3kG2RXo7ToCAaRc7dvnynByJt"; + byte[] signature = Base58.decode(signature58); + + System.out.println("Navigating to Block from transaction " + signature58); + + PaymentTransaction paymentTransaction = PaymentTransaction.fromSignature(connection, signature); + + assertNotNull(paymentTransaction); + + Block block = paymentTransaction.getBlock(connection); + + assertNotNull(block); + + System.out.println("Block " + block.getHeight() + ", signature: " + Base58.encode(block.getSignature())); + + assertEquals(49778, block.getHeight()); + } + +} diff --git a/src/test/save.java b/src/test/save.java index 0e7d9a2a..f2506a20 100644 --- a/src/test/save.java +++ b/src/test/save.java @@ -12,6 +12,7 @@ import org.junit.After; import org.junit.Before; import org.junit.Test; +import qora.account.PublicKeyAccount; import qora.transaction.PaymentTransaction; import utils.Base58; @@ -41,8 +42,9 @@ public class save { byte[] reference = Base58.decode(reference58); String signature58 = "ssss"; byte[] signature = Base58.decode(signature58); + PublicKeyAccount sender = new PublicKeyAccount("Qsender".getBytes()); - PaymentTransaction paymentTransaction = new PaymentTransaction("Qsender", "Qrecipient", BigDecimal.valueOf(12345L), BigDecimal.ONE, + PaymentTransaction paymentTransaction = new PaymentTransaction(sender, "Qrecipient", BigDecimal.valueOf(12345L), BigDecimal.ONE, Instant.now().getEpochSecond(), reference, signature); paymentTransaction.save(connection); diff --git a/src/test/updates.java b/src/test/updates.java index f0363bbb..a12c1045 100644 --- a/src/test/updates.java +++ b/src/test/updates.java @@ -47,6 +47,7 @@ public class updates { 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)"); @@ -65,21 +66,24 @@ public class updates { 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 QoraAddress NOT NULL, generation_signature Signature NOT NULL, AT_data VARBINARY(20000), AT_fees QoraAmount)"); + + "generator QoraPublicKey NOT NULL, generation_signature Signature NOT NULL, AT_data VARBINARY(20000), AT_fees QoraAmount)"); stmt.execute("CREATE INDEX BlockHeightIndex ON Blocks (height)"); stmt.execute("CREATE INDEX BlockGeneratorIndex ON Blocks (generator)"); + stmt.execute("CREATE INDEX BlockReferenceIndex ON Blocks (reference)"); break; case 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 QoraAddress, creation TIMESTAMP NOT NULL, fee QoraAmount NOT NULL, milestone_block BlockSignature)"); + + "creator QoraPublicKey, creation TIMESTAMP NOT NULL, fee QoraAmount NOT NULL, milestone_block BlockSignature)"); stmt.execute("CREATE INDEX TransactionTypeIndex ON Transactions (type)"); stmt.execute("CREATE INDEX TransactionCreationIndex ON Transactions (creation)"); + stmt.execute("CREATE INDEX TransactionCreatorIndex ON Transactions (creator)"); + stmt.execute("CREATE INDEX TransactionReferenceIndex ON Transactions (reference)"); // Transaction-Block mapping ("signature" is unique as a transaction cannot be included in more than one block) - stmt.execute("CREATE TABLE BlockTransactions (block BlockSignature, sequence INTEGER, signature Signature, " - + "PRIMARY KEY (block, sequence), FOREIGN KEY (signature) REFERENCES Transactions (signature) ON DELETE CASCADE, " - + "FOREIGN KEY (block) REFERENCES Blocks (signature) ON DELETE CASCADE)"); + stmt.execute("CREATE TABLE BlockTransactions (block_signature BlockSignature, sequence INTEGER, transaction_signature Signature, " + + "PRIMARY KEY (block_signature, sequence), FOREIGN KEY (transaction_signature) REFERENCES Transactions (signature) ON DELETE CASCADE, " + + "FOREIGN KEY (block_signature) REFERENCES Blocks (signature) ON DELETE CASCADE)"); // Unconfirmed transactions // Do we need this? If a transaction doesn't have a corresponding BlockTransactions record then it's unconfirmed? stmt.execute("CREATE TABLE UnconfirmedTransactions (signature Signature PRIMARY KEY, expiry TIMESTAMP NOT NULL)"); @@ -98,47 +102,47 @@ public class updates { case 4: // Payment Transactions - stmt.execute("CREATE TABLE PaymentTransactions (signature Signature, sender QoraAddress NOT NULL, recipient QoraAddress NOT NULL, " + 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 QoraAddress NOT NULL, name RegisteredName NOT NULL, " + 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 QoraAddress NOT NULL, name RegisteredName NOT NULL, " + 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 QoraAddress NOT NULL, name RegisteredName NOT NULL, " + 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 QoraAddress NOT NULL, name RegisteredName NOT NULL, " + 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 QoraAddress NOT NULL, name RegisteredName NOT NULL, " + 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 QoraAddress NOT NULL, poll PollName NOT NULL, " + 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 @@ -149,21 +153,21 @@ public class updates { case 11: // Vote On Poll Transactions - stmt.execute("CREATE TABLE VoteOnPollTransactions (signature Signature, voter QoraAddress NOT NULL, poll PollName NOT NULL, " + 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 QoraAddress NOT NULL, " + 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 QoraAddress NOT NULL, service TINYINT NOT NULL, " + 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 @@ -172,7 +176,7 @@ public class updates { case 14: // Issue Asset Transactions - stmt.execute("CREATE TABLE IssueAssetTransactions (signature Signature, creator QoraAddress NOT NULL, asset_name AssetName NOT NULL, " + 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 @@ -180,35 +184,34 @@ public class updates { case 15: // Transfer Asset Transactions - stmt.execute("CREATE TABLE TransferAssetTransactions (signature Signature, sender QoraAddress NOT NULL, recipient QoraAddress NOT NULL, " + 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 QoraAddress NOT NULL, " - + "have_asset AssetID NOT NULL, have_amount QoraAmount NOT NULL, " - + "want_asset AssetID NOT NULL, want_amount QoraAmount NOT NULL, " + 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 QoraAddress NOT NULL, " + 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 QoraAddress NOT NULL, " + 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 QoraAddress NOT NULL, AT_name ATName NOT NULL, " + 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)"); @@ -216,7 +219,7 @@ public class updates { case 20: // Message Transactions - stmt.execute("CREATE TABLE MessageTransactions (signature Signature, sender QoraAddress NOT NULL, recipient QoraAddress NOT NULL, " + 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;