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;