Browse Source

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});
pull/67/head
catbref 6 years ago
parent
commit
b90a486039
  1. 21
      src/brokenmd160.java
  2. 59
      src/database/DB.java
  3. 4
      src/database/NoDataFoundException.java
  4. 8
      src/qora/account/Account.java
  5. 11
      src/qora/account/PrivateKeyAccount.java
  6. 9
      src/qora/account/PublicKeyAccount.java
  7. 175
      src/qora/block/Block.java
  8. 150
      src/qora/block/BlockTransaction.java
  9. 4
      src/qora/crypto/BrokenMD160.java
  10. 43
      src/qora/crypto/Crypto.java
  11. 44
      src/qora/transaction/PaymentTransaction.java
  12. 84
      src/qora/transaction/Transaction.java
  13. 18
      src/qora/transaction/TransactionFactory.java
  14. 2
      src/settings/Settings.java
  15. 2
      src/test/crypto.java
  16. 40
      src/test/load.java
  17. 96
      src/test/migrate.java
  18. 54
      src/test/navigation.java
  19. 4
      src/test/save.java
  20. 51
      src/test/updates.java

21
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 <hex>\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());
}
}

59
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.
* <p>
* Full form is:
* <p>
* INSERT INTO <I>table</I> (<I>column</I>, ...) VALUES (?, ...) ON DUPLICATE KEY UPDATE <I>column</I>=?, ...
* <p>
* 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 ...
* <p>
* Note that each object is bound to <b>two</b> place-holders based on this SQL syntax:
* <p>
* INSERT INTO <I>table</I> (<I>column</I>, ...) VALUES (<b>?</b>, ...) ON DUPLICATE KEY UPDATE <I>column</I>=<b>?</b>, ...
*
* @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;
}
}

4
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 {

8
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))

11
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;
}
}
}

9
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;
}
}
}

175
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<Transaction> 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<Transaction> getTransactions() {
return this.transactions;
}
public List<Transaction> getTransactions(Connection connection) throws SQLException {
// Already loaded?
if (this.transactions != null)
return this.transactions;
// Load from DB
this.transactions = new ArrayList<Transaction>();
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
}
}

150
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
}

4
src/qora/crypto/BrokenMD160.java

@ -1,9 +1,9 @@
package qora.crypto;
/**
* BROKEN RIPEMD160
* <b>BROKEN RIPEMD160</b>
* <p>
* DO NOT USE in future code as this implementation is BROKEN and returns incorrect digests for some inputs.
* <b>DO NOT USE in future code</b> as this implementation is BROKEN and returns incorrect digests for some inputs.
* <p>
* It is only "grand-fathered" here to produce correct QORA addresses.
*/

43
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<byte[], byte[]> 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;
}
}
}

44
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
}
}

84
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.
* <p>
* Note that the transaction type is <b>not</b> 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();

18
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;

2
src/settings/Settings.java

@ -5,7 +5,7 @@ public class Settings {
public static Settings getInstance() {
return new Settings();
}
public int getMaxBytePerFee() {
return 1024;
}

2
src/test/crypto.java

@ -35,5 +35,5 @@ public class crypto {
assertEquals(expected, Crypto.toAddress(publicKey));
}
}

40
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();
}
}
}

96
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<String, byte[]> publicKeyByAddress = new HashMap<String, byte[]>();
@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);

54
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());
}
}

4
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);

51
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;

Loading…
Cancel
Save