Merge branch 'DAO' into DAO

This commit is contained in:
catbref 2018-06-12 08:34:03 +01:00 committed by GitHub
commit 6853f0edcb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 871 additions and 1064 deletions

View File

@ -1,6 +1,6 @@
package data.account;
public class Account {
public class AccountData {
// Properties
protected String address;
@ -8,10 +8,10 @@ public class Account {
// Constructors
protected Account() {
protected AccountData() {
}
public Account(String address) {
public AccountData(String address) {
this.address = address;
}
@ -33,10 +33,10 @@ public class Account {
@Override
public boolean equals(Object b) {
if (!(b instanceof Account))
if (!(b instanceof AccountData))
return false;
return this.getAddress().equals(((Account) b).getAddress());
return this.getAddress().equals(((AccountData) b).getAddress());
}
@Override

View File

@ -1,9 +0,0 @@
package data.account;
public final class GenesisAccount extends PublicKeyAccount {
public GenesisAccount() {
super(new byte[] { 1, 1, 1, 1, 1, 1, 1, 1 });
}
}

View File

@ -1,27 +0,0 @@
package data.account;
import qora.crypto.Crypto;
public class PublicKeyAccount extends Account {
// Properties
protected byte[] publicKey;
// Constructors
public PublicKeyAccount(byte[] publicKey) {
super(Crypto.toAddress(publicKey));
this.publicKey = publicKey;
}
protected PublicKeyAccount() {
}
// Getters/Setters
public byte[] getPublicKey() {
return this.publicKey;
}
}

View File

@ -1,86 +0,0 @@
package data.block;
import java.math.BigDecimal;
import qora.account.PublicKeyAccount;
public class Block implements BlockData {
private int version;
private byte[] reference;
private int transactionCount;
private BigDecimal totalFees;
private byte[] transactionsSignature;
private int height;
private long timestamp;
private BigDecimal generatingBalance;
private byte[] generatorPublicKey;
private byte[] generatorSignature;
private byte[] atBytes;
private BigDecimal atFees;
public Block(int version, byte[] reference, int transactionCount, BigDecimal totalFees, byte[] transactionsSignature,
int height, long timestamp, BigDecimal generatingBalance, byte[] generatorPublicKey, byte[] generatorSignature,
byte[] atBytes, BigDecimal atFees)
{
this.version = version;
this.reference = reference;
this.transactionCount = transactionCount;
this.totalFees = totalFees;
this.transactionsSignature = transactionsSignature;
this.height = height;
this.timestamp = timestamp;
this.generatingBalance = generatingBalance;
this.generatorPublicKey = generatorPublicKey;
this.generatorSignature = generatorSignature;
this.atBytes = atBytes;
this.atFees = atFees;
}
public int getVersion() {
return version;
}
public byte[] getReference() {
return reference;
}
public int getTransactionCount() {
return transactionCount;
}
public BigDecimal getTotalFees() {
return totalFees;
}
public byte[] getTransactionsSignature() {
return transactionsSignature;
}
public int getHeight() {
return height;
}
public long getTimestamp() {
return timestamp;
}
public BigDecimal getGeneratingBalance() {
return generatingBalance;
}
public byte[] getGeneratorPublicKey() {
return generatorPublicKey;
}
public byte[] getGeneratorSignature() {
return generatorSignature;
}
public byte[] getAtBytes() {
return atBytes;
}
public BigDecimal getAtFees() {
return atFees;
}
}

View File

@ -2,17 +2,119 @@ package data.block;
import java.math.BigDecimal;
public interface BlockData {
public int getVersion();
public byte[] getReference();
public int getTransactionCount();
public BigDecimal getTotalFees();
public byte[] getTransactionsSignature();
public int getHeight();
public long getTimestamp();
public BigDecimal getGeneratingBalance();
public byte[] getGeneratorPublicKey();
public byte[] getGeneratorSignature();
public byte[] getAtBytes();
public BigDecimal getAtFees();
import com.google.common.primitives.Bytes;
public class BlockData {
private byte[] signature;
private int version;
private byte[] reference;
private int transactionCount;
private BigDecimal totalFees;
private byte[] transactionsSignature;
private int height;
private long timestamp;
private BigDecimal generatingBalance;
private byte[] generatorPublicKey;
private byte[] generatorSignature;
private byte[] atBytes;
private BigDecimal atFees;
public BlockData(int version, byte[] reference, int transactionCount, BigDecimal totalFees, byte[] transactionsSignature, int height, long timestamp,
BigDecimal generatingBalance, byte[] generatorPublicKey, byte[] generatorSignature, byte[] atBytes, BigDecimal atFees) {
this.version = version;
this.reference = reference;
this.transactionCount = transactionCount;
this.totalFees = totalFees;
this.transactionsSignature = transactionsSignature;
this.height = height;
this.timestamp = timestamp;
this.generatingBalance = generatingBalance;
this.generatorPublicKey = generatorPublicKey;
this.generatorSignature = generatorSignature;
this.atBytes = atBytes;
this.atFees = atFees;
if (this.generatorSignature != null && this.transactionsSignature != null)
this.signature = Bytes.concat(this.generatorSignature, this.transactionsSignature);
else
this.signature = null;
}
public int getTransactionCount() {
return this.transactionCount;
}
public void setTransactionCount(int transactionCount) {
this.transactionCount = transactionCount;
}
public BigDecimal getTotalFees() {
return this.totalFees;
}
public void setTotalFees(BigDecimal totalFees) {
this.totalFees = totalFees;
}
public byte[] getTransactionsSignature() {
return this.transactionsSignature;
}
public void setTransactionsSignature(byte[] transactionsSignature) {
this.transactionsSignature = transactionsSignature;
}
public byte[] getSignature() {
return this.signature;
}
public int getVersion() {
return this.version;
}
public byte[] getReference() {
return this.reference;
}
public void setReference(byte[] reference) {
this.reference = reference;
}
public int getHeight() {
return this.height;
}
public void setHeight(int height) {
this.height = height;
}
public long getTimestamp() {
return this.timestamp;
}
public BigDecimal getGeneratingBalance() {
return this.generatingBalance;
}
public byte[] getGeneratorPublicKey() {
return this.generatorPublicKey;
}
public byte[] getGeneratorSignature() {
return this.generatorSignature;
}
public void setGeneratorSignature(byte[] generatorSignature) {
this.generatorSignature = generatorSignature;
}
public byte[] getAtBytes() {
return this.atBytes;
}
public BigDecimal getAtFees() {
return this.atFees;
}
}

View File

@ -1,37 +0,0 @@
package data.transaction;
import java.math.BigDecimal;
import data.account.Account;
import data.account.GenesisAccount;
public class GenesisTransaction extends Transaction {
// Properties
private Account recipient;
private BigDecimal amount;
// Constructors
public GenesisTransaction(Account recipient, BigDecimal amount, long timestamp, byte[] signature) {
super(TransactionType.GENESIS, BigDecimal.ZERO, new GenesisAccount(), timestamp, signature);
this.recipient = recipient;
this.amount = amount;
}
public GenesisTransaction(Account recipient, BigDecimal amount, long timestamp) {
this(recipient, amount, timestamp, null);
}
// Getters/Setters
public Account getRecipient() {
return this.recipient;
}
public BigDecimal getAmount() {
return this.amount;
}
}

View File

@ -0,0 +1,37 @@
package data.transaction;
import java.math.BigDecimal;
import qora.account.GenesisAccount;
import qora.transaction.Transaction.TransactionType;
public class GenesisTransactionData extends TransactionData {
// Properties
private String recipient;
private BigDecimal amount;
// Constructors
public GenesisTransactionData(String recipient, BigDecimal amount, long timestamp, byte[] signature) {
super(TransactionType.GENESIS, BigDecimal.ZERO, new GenesisAccount().getPublicKey(), timestamp, signature);
this.recipient = recipient;
this.amount = amount;
}
public GenesisTransactionData(String recipient, BigDecimal amount, long timestamp) {
this(recipient, amount, timestamp, null);
}
// Getters/Setters
public String getRecipient() {
return this.recipient;
}
public BigDecimal getAmount() {
return this.amount;
}
}

View File

@ -1,83 +0,0 @@
package data.transaction;
import java.math.BigDecimal;
import java.util.Map;
import data.account.PublicKeyAccount;
import static java.util.Arrays.stream;
import static java.util.stream.Collectors.toMap;
public abstract class Transaction {
// Transaction types
// TODO Transaction types are semantic and should go into the business logic layer.
// No need to know the meaning of the integer value in data layer
public enum TransactionType {
GENESIS(1), PAYMENT(2), REGISTER_NAME(3), UPDATE_NAME(4), SELL_NAME(5), CANCEL_SELL_NAME(6), BUY_NAME(7), CREATE_POLL(8), VOTE_ON_POLL(9), ARBITRARY(
10), ISSUE_ASSET(11), TRANSFER_ASSET(12), CREATE_ASSET_ORDER(13), CANCEL_ASSET_ORDER(14), MULTIPAYMENT(15), DEPLOY_AT(16), MESSAGE(17);
public final int value;
private final static Map<Integer, TransactionType> map = stream(TransactionType.values()).collect(toMap(type -> type.value, type -> type));
TransactionType(int value) {
this.value = value;
}
public static TransactionType valueOf(int value) {
return map.get(value);
}
}
// Properties shared with all transaction types
protected TransactionType type;
// TODO PublicKeyAccount is a separate data entity, so here should only be a key to reference it
protected PublicKeyAccount creator;
protected long timestamp;
protected byte[] reference;
protected BigDecimal fee;
protected byte[] signature;
// Constructors
public Transaction(TransactionType type, BigDecimal fee, PublicKeyAccount creator, long timestamp, byte[] reference, byte[] signature) {
this.fee = fee;
this.type = type;
this.creator = creator;
this.timestamp = timestamp;
this.reference = reference;
this.signature = signature;
}
public Transaction(TransactionType type, BigDecimal fee, PublicKeyAccount creator, long timestamp, byte[] reference) {
this(type, fee, creator, timestamp, reference, null);
}
// Getters/setters
public TransactionType getType() {
return this.type;
}
public PublicKeyAccount getCreator() {
return this.creator;
}
public long getTimestamp() {
return this.timestamp;
}
public byte[] getReference() {
return this.reference;
}
public BigDecimal getFee() {
return this.fee;
}
public byte[] getSignature() {
return this.signature;
}
}

View File

@ -0,0 +1,58 @@
package data.transaction;
import java.math.BigDecimal;
import qora.transaction.Transaction.TransactionType;
public abstract class TransactionData {
// Properties shared with all transaction types
protected TransactionType type;
protected byte[] creatorPublicKey;
protected long timestamp;
protected byte[] reference;
protected BigDecimal fee;
protected byte[] signature;
// Constructors
public TransactionData(TransactionType type, BigDecimal fee, byte[] creatorPublicKey, long timestamp, byte[] reference, byte[] signature) {
this.fee = fee;
this.type = type;
this.creatorPublicKey = creatorPublicKey;
this.timestamp = timestamp;
this.reference = reference;
this.signature = signature;
}
public TransactionData(TransactionType type, BigDecimal fee, byte[] creatorPublicKey, long timestamp, byte[] reference) {
this(type, fee, creatorPublicKey, timestamp, reference, null);
}
// Getters/setters
public TransactionType getType() {
return this.type;
}
public byte[] getCreatorPublicKey() {
return this.creatorPublicKey;
}
public long getTimestamp() {
return this.timestamp;
}
public byte[] getReference() {
return this.reference;
}
public BigDecimal getFee() {
return this.fee;
}
public byte[] getSignature() {
return this.signature;
}
}

View File

@ -8,8 +8,9 @@ public class PublicKeyAccount extends Account {
protected byte[] publicKey;
public PublicKeyAccount(byte[] publicKey) {
super(Crypto.toAddress(publicKey));
this.publicKey = publicKey;
this.address = Crypto.toAddress(this.publicKey);
}
protected PublicKeyAccount() {

View File

@ -6,17 +6,8 @@ import java.sql.SQLException;
import database.DB;
import database.NoDataFoundException;
import qora.account.Account;
import qora.transaction.TransactionHandler;
import repository.hsqldb.HSQLDBSaver;
/*
* TODO:
* Probably need to standardize on using assetId or assetKey for the long value, and plain "asset" for the java object.
* Thus in the database the primary key column could be called "asset_id".
* In the Order object, we'd pass longs to variables with names like "haveAssetId" and use getters like "getHaveAssetId"
* which frees up other method names like "getHaveAsset" to return a java Asset object.
*/
public class Asset {
public static final long QORA = 0L;
@ -91,7 +82,7 @@ public class Asset {
this.description = rs.getString(3);
this.quantity = rs.getLong(4);
this.isDivisible = rs.getBoolean(5);
this.reference = DB.getResultSetBytes(rs.getBinaryStream(6), TransactionHandler.REFERENCE_LENGTH);
this.reference = DB.getResultSetBytes(rs.getBinaryStream(6));
}
public static Asset fromAssetId(long assetId) throws SQLException {

View File

@ -22,6 +22,8 @@ import com.google.common.primitives.Bytes;
import com.google.common.primitives.Ints;
import com.google.common.primitives.Longs;
import data.block.BlockData;
import data.transaction.TransactionData;
import database.DB;
import database.NoDataFoundException;
import qora.account.PrivateKeyAccount;
@ -31,10 +33,14 @@ import qora.assets.Order;
import qora.assets.Trade;
import qora.transaction.CreateOrderTransaction;
import qora.transaction.GenesisTransaction;
import qora.transaction.TransactionHandler;
import qora.transaction.Transaction;
import repository.BlockRepository;
import repository.DataException;
import repository.RepositoryManager;
import repository.hsqldb.HSQLDBSaver;
import qora.transaction.TransactionHandler;
import transform.TransformationException;
import transform.block.BlockTransformer;
import transform.transaction.TransactionTransformer;
import utils.Base58;
import utils.NTP;
import utils.Serialization;
@ -63,51 +69,16 @@ import utils.Serialization;
public class Block {
/**
* Ordered list of columns when fetching a Block row from database.
*/
private static final String DB_COLUMNS = "version, reference, transaction_count, total_fees, "
+ "transactions_signature, height, generation, generating_balance, generator, generator_signature, AT_data, AT_fees";
// Database properties
protected int version;
protected byte[] reference;
protected int transactionCount;
protected BigDecimal totalFees;
protected byte[] transactionsSignature;
protected int height;
protected long timestamp;
protected BigDecimal generatingBalance;
protected PublicKeyAccount generator;
protected byte[] generatorSignature;
protected byte[] atBytes;
protected BigDecimal atFees;
// Properties
private BlockData blockData;
private PublicKeyAccount generator;
// Other properties
protected List<TransactionHandler> transactions;
protected List<Transaction> transactions;
protected BigDecimal cachedNextGeneratingBalance;
// Property lengths for serialisation
protected static final int VERSION_LENGTH = 4;
protected static final int TRANSACTIONS_SIGNATURE_LENGTH = 64;
protected static final int GENERATOR_SIGNATURE_LENGTH = 64;
protected static final int REFERENCE_LENGTH = GENERATOR_SIGNATURE_LENGTH + TRANSACTIONS_SIGNATURE_LENGTH;
protected static final int TIMESTAMP_LENGTH = 8;
protected static final int GENERATING_BALANCE_LENGTH = 8;
protected static final int GENERATOR_LENGTH = 32;
protected static final int TRANSACTION_COUNT_LENGTH = 4;
protected static final int BASE_LENGTH = VERSION_LENGTH + REFERENCE_LENGTH + TIMESTAMP_LENGTH + GENERATING_BALANCE_LENGTH + GENERATOR_LENGTH
+ TRANSACTIONS_SIGNATURE_LENGTH + GENERATOR_SIGNATURE_LENGTH + TRANSACTION_COUNT_LENGTH;
// Other length constants
protected static final int BLOCK_SIGNATURE_LENGTH = GENERATOR_SIGNATURE_LENGTH + TRANSACTIONS_SIGNATURE_LENGTH;
public static final int MAX_BLOCK_BYTES = 1048576;
protected static final int TRANSACTION_SIZE_LENGTH = 4; // per transaction
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;
// Other useful constants
public static final int MAX_BLOCK_BYTES = 1048576;
/**
* Number of blocks between recalculating block's generating balance.
*/
@ -125,91 +96,15 @@ public class Block {
// Constructors
// For creating a new block from scratch
public Block(int version, byte[] reference, long timestamp, BigDecimal generatingBalance, PublicKeyAccount generator, byte[] atBytes, BigDecimal atFees) {
this.version = version;
this.reference = reference;
this.timestamp = timestamp;
this.generatingBalance = generatingBalance;
this.generator = generator;
this.generatorSignature = null;
this.height = 0;
this.transactionCount = 0;
this.transactions = new ArrayList<TransactionHandler>();
this.transactionsSignature = null;
this.totalFees = BigDecimal.ZERO.setScale(8);
this.atBytes = atBytes;
this.atFees = atFees;
if (this.atFees != null)
this.totalFees = this.totalFees.add(this.atFees);
}
// For instantiating a block that was previously serialized
protected Block(int version, byte[] reference, long timestamp, BigDecimal generatingBalance, PublicKeyAccount generator, byte[] generatorSignature,
byte[] transactionsSignature, byte[] atBytes, BigDecimal atFees, List<TransactionHandler> transactions) {
this(version, reference, timestamp, generatingBalance, generator, atBytes, atFees);
this.generatorSignature = generatorSignature;
this.transactionsSignature = transactionsSignature;
this.transactionCount = transactions.size();
this.transactions = transactions;
// Add transactions' fees to totalFees
for (TransactionHandler transaction : this.transactions)
this.totalFees = this.totalFees.add(transaction.getFee());
public Block(BlockData blockData) {
this.blockData = blockData;
this.generator = new PublicKeyAccount(blockData.getGeneratorPublicKey());
}
// Getters/setters
public int getVersion() {
return this.version;
}
public byte[] getReference() {
return this.reference;
}
public long getTimestamp() {
return this.timestamp;
}
public BigDecimal getGeneratingBalance() {
return this.generatingBalance;
}
public PublicKeyAccount getGenerator() {
return this.generator;
}
public byte[] getGeneratorSignature() {
return this.generatorSignature;
}
public byte[] getTransactionsSignature() {
return this.transactionsSignature;
}
public BigDecimal getTotalFees() {
return this.totalFees;
}
public int getTransactionCount() {
return this.transactionCount;
}
public byte[] getATBytes() {
return this.atBytes;
}
public BigDecimal getATFees() {
return this.atFees;
}
public int getHeight() {
return this.height;
public BlockData getBlockData() {
return this.blockData;
}
// More information
@ -220,26 +115,10 @@ public class Block {
* @return byte[], or null if either component signature is null.
*/
public byte[] getSignature() {
if (this.generatorSignature == null || this.transactionsSignature == null)
if (this.blockData.getGeneratorSignature() == null || this.blockData.getTransactionsSignature() == null)
return null;
return Bytes.concat(this.generatorSignature, this.transactionsSignature);
}
public int getDataLength() {
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 (TransactionHandler transaction : this.transactions)
blockLength += TRANSACTION_SIZE_LENGTH + transaction.getDataLength();
return blockLength;
return Bytes.concat(this.blockData.getGeneratorSignature(), this.blockData.getTransactionsSignature());
}
/**
@ -248,9 +127,9 @@ public class Block {
* @return 1, 2 or 3
*/
public int getNextBlockVersion() {
if (this.height < AT_BLOCK_HEIGHT_RELEASE)
if (this.blockData.getHeight() < AT_BLOCK_HEIGHT_RELEASE)
return 1;
else if (this.timestamp < POWFIX_RELEASE_TIMESTAMP)
else if (this.blockData.getTimestamp() < POWFIX_RELEASE_TIMESTAMP)
return 2;
else
return 3;
@ -269,8 +148,8 @@ public class Block {
*/
public BigDecimal getNextBlockGeneratingBalance() throws SQLException {
// This block not at the start of an interval?
if (this.height % BLOCK_RETARGET_INTERVAL != 0)
return this.generatingBalance;
if (this.blockData.getHeight() % BLOCK_RETARGET_INTERVAL != 0)
return this.blockData.getGeneratingBalance();
// Return cached calculation if we have one
if (this.cachedNextGeneratingBalance != null)
@ -280,23 +159,29 @@ public class Block {
// Navigate back to first block in previous interval:
// XXX: why can't we simply load using block height?
Block firstBlock = this;
for (int i = 1; firstBlock != null && i < BLOCK_RETARGET_INTERVAL; ++i)
firstBlock = firstBlock.getParent();
BlockRepository blockRepo = RepositoryManager.getBlockRepository();
BlockData firstBlock = this.blockData;
try {
for (int i = 1; firstBlock != null && i < BLOCK_RETARGET_INTERVAL; ++i)
firstBlock = blockRepo.fromSignature(firstBlock.getReference());
} catch (DataException e) {
firstBlock = null;
}
// Couldn't navigate back far enough?
if (firstBlock == null)
throw new IllegalStateException("Failed to calculate next block's generating balance due to lack of historic blocks");
// Calculate the actual time period (in ms) over previous interval's blocks.
long previousGeneratingTime = this.timestamp - firstBlock.getTimestamp();
long previousGeneratingTime = this.blockData.getTimestamp() - firstBlock.getTimestamp();
// Calculate expected forging time (in ms) for a whole interval based on this block's generating balance.
long expectedGeneratingTime = Block.calcForgingDelay(this.generatingBalance) * BLOCK_RETARGET_INTERVAL * 1000;
long expectedGeneratingTime = Block.calcForgingDelay(this.blockData.getGeneratingBalance()) * BLOCK_RETARGET_INTERVAL * 1000;
// Finally, scale generating balance such that faster than expected previous intervals produce larger generating balances.
BigDecimal multiplier = BigDecimal.valueOf((double) expectedGeneratingTime / (double) previousGeneratingTime);
this.cachedNextGeneratingBalance = BlockChain.minMaxBalance(this.generatingBalance.multiply(multiplier));
this.cachedNextGeneratingBalance = BlockChain.minMaxBalance(this.blockData.getGeneratingBalance().multiply(multiplier));
return this.cachedNextGeneratingBalance;
}
@ -316,310 +201,31 @@ public class Block {
/**
* Return block's transactions.
* <p>
* If the block was loaded from DB then it's possible this method will call the DB to load the transactions if they are not already loaded.
* If the block was loaded from repository then it's possible this method will call the repository to load the transactions if they are not already loaded.
*
* @return
* @throws SQLException
* @throws DataException
*/
public List<TransactionHandler> getTransactions() throws SQLException {
public List<Transaction> getTransactions() throws DataException {
// Already loaded?
if (this.transactions != null)
return this.transactions;
// Allocate cache for results
this.transactions = new ArrayList<TransactionHandler>();
List<TransactionData> transactionsData = RepositoryManager.getBlockRepository().getTransactionsFromSignature(this.blockData.getSignature());
ResultSet rs = DB.checkedExecute("SELECT transaction_signature FROM BlockTransactions WHERE block_signature = ?", this.getSignature());
if (rs == null)
return this.transactions; // No transactions in this block
// NB: do-while loop because DB.checkedExecute() implicitly calls ResultSet.next() for us
do {
byte[] transactionSignature = DB.getResultSetBytes(rs.getBinaryStream(1), TransactionHandler.SIGNATURE_LENGTH);
this.transactions.add(TransactionFactory.fromSignature(transactionSignature));
// No need to update totalFees as this will be loaded via the Blocks table
} while (rs.next());
// The number of transactions fetched from database should correspond with Block's transactionCount
if (this.transactions.size() != this.transactionCount)
throw new IllegalStateException("Block's transactions from database do not match block's transaction count");
// The number of transactions fetched from repository should correspond with Block's transactionCount
if (transactionsData.size() != this.blockData.getTransactionCount())
throw new IllegalStateException("Block's transactions from repository do not match block's transaction count");
this.transactions = new ArrayList<Transaction>();
for (TransactionData transactionData : transactionsData)
this.transactions.add(Transaction.fromData(transactionData));
return this.transactions;
}
// Load/Save
protected Block(byte[] signature) throws SQLException {
this(DB.checkedExecute("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);
this.transactionCount = rs.getInt(3);
this.totalFees = rs.getBigDecimal(4);
this.transactionsSignature = DB.getResultSetBytes(rs.getBinaryStream(5), TRANSACTIONS_SIGNATURE_LENGTH);
this.height = rs.getInt(6);
this.timestamp = rs.getTimestamp(7).getTime();
this.generatingBalance = rs.getBigDecimal(8);
// Note: can't use GENERATOR_LENGTH in case we encounter Genesis Account's short, 8-byte public key
this.generator = new PublicKeyAccount(DB.getResultSetBytes(rs.getBinaryStream(9)));
this.generatorSignature = DB.getResultSetBytes(rs.getBinaryStream(10), GENERATOR_SIGNATURE_LENGTH);
this.atBytes = DB.getResultSetBytes(rs.getBinaryStream(11));
this.atFees = rs.getBigDecimal(12);
}
/**
* Load Block from DB using block signature.
*
* @param signature
* @return Block, or null if not found
* @throws SQLException
*/
public static Block fromSignature(byte[] signature) throws SQLException {
try {
return new Block(signature);
} catch (NoDataFoundException e) {
return null;
}
}
/**
* Load Block from DB using block height
*
* @param height
* @return Block, or null if not found
* @throws SQLException
*/
public static Block fromHeight(int height) throws SQLException {
PreparedStatement preparedStatement = DB.getConnection().prepareStatement("SELECT " + DB_COLUMNS + " FROM Blocks WHERE height = ?");
preparedStatement.setInt(1, height);
try {
return new Block(DB.checkedExecute(preparedStatement));
} catch (NoDataFoundException e) {
return null;
}
}
protected void save() throws SQLException {
HSQLDBSaver saveHelper = new HSQLDBSaver("Blocks");
saveHelper.bind("signature", this.getSignature()).bind("version", this.version).bind("reference", this.reference)
.bind("transaction_count", this.transactionCount).bind("total_fees", this.totalFees).bind("transactions_signature", this.transactionsSignature)
.bind("height", this.height).bind("generation", new Timestamp(this.timestamp)).bind("generating_balance", this.generatingBalance)
.bind("generator", this.generator.getPublicKey()).bind("generator_signature", this.generatorSignature).bind("AT_data", this.atBytes)
.bind("AT_fees", this.atFees);
saveHelper.execute();
}
// Navigation
/**
* Load parent Block from DB
*
* @return Block, or null if not found
* @throws SQLException
*/
public Block getParent() throws SQLException {
try {
return new Block(this.reference);
} catch (NoDataFoundException e) {
return null;
}
}
/**
* Load child Block from DB
*
* @return Block, or null if not found
* @throws SQLException
*/
public Block getChild() throws SQLException {
byte[] blockSignature = this.getSignature();
if (blockSignature == null)
return null;
ResultSet resultSet = DB.checkedExecute("SELECT " + DB_COLUMNS + " FROM Blocks WHERE reference = ?", blockSignature);
try {
return new Block(resultSet);
} catch (NoDataFoundException e) {
return null;
}
}
// Converters
@SuppressWarnings("unchecked")
public JSONObject toJSON() throws SQLException {
JSONObject json = new JSONObject();
json.put("version", this.version);
json.put("timestamp", this.timestamp);
json.put("generatingBalance", this.generatingBalance);
json.put("generator", this.generator.getAddress());
json.put("generatorPublicKey", Base58.encode(this.generator.getPublicKey()));
json.put("fee", this.getTotalFees().toPlainString());
json.put("transactionsSignature", Base58.encode(this.transactionsSignature));
json.put("generatorSignature", Base58.encode(this.generatorSignature));
json.put("signature", Base58.encode(this.getSignature()));
if (this.reference != null)
json.put("reference", Base58.encode(this.reference));
json.put("height", this.getHeight());
// Add transaction info
JSONArray transactionsJson = new JSONArray();
boolean tradesHappened = false;
for (TransactionHandler transaction : this.getTransactions()) {
transactionsJson.add(transaction.toJSON());
// If this is an asset CreateOrderTransaction then check to see if any trades happened
if (transaction.getType() == Transaction.TransactionHandler.CREATE_ASSET_ORDER) {
CreateOrderTransaction orderTransaction = (CreateOrderTransaction) transaction;
Order order = orderTransaction.getOrder();
List<Trade> trades = order.getTrades();
// Filter out trades with timestamps that don't match order transaction's timestamp
trades.removeIf((Trade trade) -> trade.getTimestamp() != order.getTimestamp());
// Any trades left?
if (!trades.isEmpty()) {
tradesHappened = true;
// No need to check any further
break;
}
}
}
json.put("transactions", transactionsJson);
// Add asset trade activity flag
json.put("assetTrades", tradesHappened);
// Add CIYAM AT info (if any)
if (atBytes != null) {
json.put("blockATs", HashCode.fromBytes(atBytes).toString());
json.put("atFees", this.atFees);
}
return json;
}
public byte[] toBytes() throws SQLException {
try {
ByteArrayOutputStream bytes = new ByteArrayOutputStream(getDataLength());
bytes.write(Ints.toByteArray(this.version));
bytes.write(Longs.toByteArray(this.timestamp));
bytes.write(this.reference);
// NOTE: generatingBalance serialized as long value, not as BigDecimal, for historic compatibility
bytes.write(Longs.toByteArray(this.generatingBalance.longValue()));
bytes.write(this.generator.getPublicKey());
bytes.write(this.transactionsSignature);
bytes.write(this.generatorSignature);
if (this.version >= 2) {
if (this.atBytes != null) {
bytes.write(Ints.toByteArray(this.atBytes.length));
bytes.write(this.atBytes);
// NOTE: atFees serialized as long value, not as BigDecimal, for historic compatibility
bytes.write(Longs.toByteArray(this.atFees.longValue()));
} else {
bytes.write(Ints.toByteArray(0));
bytes.write(Longs.toByteArray(0L));
}
}
// Transactions
bytes.write(Ints.toByteArray(this.transactionCount));
for (TransactionHandler transaction : this.getTransactions()) {
bytes.write(Ints.toByteArray(transaction.getDataLength()));
bytes.write(transaction.toBytes());
}
return bytes.toByteArray();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
public static Block parse(byte[] data) throws TransformationException {
if (data == null)
return null;
if (data.length < BASE_LENGTH)
throw new TransformationException("Byte data too short for Block");
ByteBuffer byteBuffer = ByteBuffer.wrap(data);
int version = byteBuffer.getInt();
if (version >= 2 && data.length < BASE_LENGTH + AT_LENGTH)
throw new TransformationException("Byte data too short for V2+ Block");
long timestamp = byteBuffer.getLong();
byte[] reference = new byte[REFERENCE_LENGTH];
byteBuffer.get(reference);
BigDecimal generatingBalance = BigDecimal.valueOf(byteBuffer.getLong()).setScale(8);
PublicKeyAccount generator = Serialization.deserializePublicKey(byteBuffer);
byte[] transactionsSignature = new byte[TRANSACTIONS_SIGNATURE_LENGTH];
byteBuffer.get(transactionsSignature);
byte[] generatorSignature = new byte[GENERATOR_SIGNATURE_LENGTH];
byteBuffer.get(generatorSignature);
byte[] atBytes = null;
BigDecimal atFees = null;
if (version >= 2) {
int atBytesLength = byteBuffer.getInt();
if (atBytesLength > MAX_BLOCK_BYTES)
throw new TransformationException("Byte data too long for Block's AT info");
atBytes = new byte[atBytesLength];
byteBuffer.get(atBytes);
atFees = BigDecimal.valueOf(byteBuffer.getLong()).setScale(8);
}
int transactionCount = byteBuffer.getInt();
// Parse transactions now, compared to deferred parsing in Gen1, so we can throw ParseException if need be
List<TransactionHandler> transactions = new ArrayList<TransactionHandler>();
for (int t = 0; t < transactionCount; ++t) {
if (byteBuffer.remaining() < TRANSACTION_SIZE_LENGTH)
throw new TransformationException("Byte data too short for Block Transaction length");
int transactionLength = byteBuffer.getInt();
if (byteBuffer.remaining() < transactionLength)
throw new TransformationException("Byte data too short for Block Transaction");
if (transactionLength > MAX_BLOCK_BYTES)
throw new TransformationException("Byte data too long for Block Transaction");
byte[] transactionBytes = new byte[transactionLength];
byteBuffer.get(transactionBytes);
TransactionHandler transaction = TransactionHandler.parse(transactionBytes);
transactions.add(transaction);
}
if (byteBuffer.hasRemaining())
throw new TransformationException("Excess byte data found after parsing Block");
return new Block(version, reference, timestamp, generatingBalance, generator, generatorSignature, transactionsSignature, atBytes, atFees, transactions);
}
// Processing
/**
@ -629,12 +235,12 @@ public class Block {
* <p>
* Requires block's {@code generator} being a {@code PrivateKeyAccount} so block's transactions signature can be recalculated.
*
* @param transaction
* @param transactionData
* @return true if transaction successfully added to block, false otherwise
* @throws IllegalStateException
* if block's {@code generator} is not a {@code PrivateKeyAccount}.
*/
public boolean addTransaction(TransactionHandler transaction) {
public boolean addTransaction(TransactionData transactionData) {
// Can't add to transactions if we haven't loaded existing ones yet
if (this.transactions == null)
throw new IllegalStateException("Attempted to add transaction to partially loaded database Block");
@ -643,17 +249,21 @@ public class Block {
throw new IllegalStateException("Block's generator has no private key");
// Check there is space in block
if (this.getDataLength() + transaction.getDataLength() > MAX_BLOCK_BYTES)
try {
if (BlockTransformer.getDataLength(this.blockData) + TransactionTransformer.getDataLength(transactionData) > MAX_BLOCK_BYTES)
return false;
} catch (TransformationException e) {
return false;
}
// Add to block
this.transactions.add(transaction);
this.transactions.add(Transaction.fromData(transactionData));
// Update transaction count
this.transactionCount++;
this.blockData.setTransactionCount(this.blockData.getTransactionCount() + 1);
// Update totalFees
this.totalFees.add(transaction.getFee());
this.blockData.setTotalFees(this.blockData.getTotalFees().add(transactionData.getFee()));
// Update transactions signature
calcTransactionsSignature();
@ -668,29 +278,17 @@ public class Block {
*
* @throws IllegalStateException
* if block's {@code generator} is not a {@code PrivateKeyAccount}.
* @throws RuntimeException
* if somehow the generator signature cannot be calculated
*/
public void calcGeneratorSignature() {
if (!(this.generator instanceof PrivateKeyAccount))
throw new IllegalStateException("Block's generator has no private key");
this.generatorSignature = ((PrivateKeyAccount) this.generator).sign(this.getBytesForGeneratorSignature());
}
private byte[] getBytesForGeneratorSignature() {
try {
ByteArrayOutputStream bytes = new ByteArrayOutputStream(GENERATOR_SIGNATURE_LENGTH + GENERATING_BALANCE_LENGTH + GENERATOR_LENGTH);
// Only copy the generator signature from reference, which is the first 64 bytes.
bytes.write(Arrays.copyOf(this.reference, GENERATOR_SIGNATURE_LENGTH));
bytes.write(Longs.toByteArray(this.generatingBalance.longValue()));
// We're padding here just in case the generator is the genesis account whose public key is only 8 bytes long.
bytes.write(Bytes.ensureCapacity(this.generator.getPublicKey(), GENERATOR_LENGTH, 0));
return bytes.toByteArray();
} catch (IOException e) {
throw new RuntimeException(e);
this.blockData.setGeneratorSignature(((PrivateKeyAccount) this.generator).sign(BlockTransformer.getBytesForGeneratorSignature(this.blockData)));
} catch (TransformationException e) {
throw new RuntimeException("Unable to calculate block's generator signature", e);
}
}
@ -701,41 +299,33 @@ public class Block {
*
* @throws IllegalStateException
* if block's {@code generator} is not a {@code PrivateKeyAccount}.
* @throws RuntimeException
* if somehow the transactions signature cannot be calculated
*/
public void calcTransactionsSignature() {
if (!(this.generator instanceof PrivateKeyAccount))
throw new IllegalStateException("Block's generator has no private key");
this.transactionsSignature = ((PrivateKeyAccount) this.generator).sign(this.getBytesForTransactionsSignature());
}
private byte[] getBytesForTransactionsSignature() {
ByteArrayOutputStream bytes = new ByteArrayOutputStream(GENERATOR_SIGNATURE_LENGTH + this.transactionCount * TransactionHandler.SIGNATURE_LENGTH);
try {
bytes.write(this.generatorSignature);
for (TransactionHandler transaction : this.getTransactions()) {
if (!transaction.isSignatureValid())
return null;
bytes.write(transaction.getSignature());
}
return bytes.toByteArray();
} catch (IOException | SQLException e) {
throw new RuntimeException(e);
this.blockData.setTransactionsSignature(((PrivateKeyAccount) this.generator).sign(BlockTransformer.getBytesForTransactionsSignature(this)));
} catch (TransformationException e) {
throw new RuntimeException("Unable to calculate block's transactions signature", e);
}
}
public boolean isSignatureValid() {
// Check generator's signature first
if (!this.generator.verify(this.generatorSignature, getBytesForGeneratorSignature()))
return false;
// Check transactions signature
if (!this.generator.verify(this.transactionsSignature, getBytesForTransactionsSignature()))
public boolean isSignatureValid() {
try {
// Check generator's signature first
if (!this.generator.verify(this.blockData.getGeneratorSignature(), BlockTransformer.getBytesForGeneratorSignature(this.blockData)))
return false;
// Check transactions signature
if (!this.generator.verify(this.blockData.getTransactionsSignature(), BlockTransformer.getBytesForTransactionsSignature(this)))
return false;
} catch (TransformationException e) {
return false;
}
return true;
}
@ -749,41 +339,44 @@ public class Block {
*
* @return true if block is valid, false otherwise.
* @throws SQLException
* @throws DataException
*/
public boolean isValid() throws SQLException {
public boolean isValid() throws SQLException, DataException {
// TODO
// Check parent blocks exists
if (this.reference == null)
if (this.blockData.getReference() == null)
return false;
Block parentBlock = this.getParent();
if (parentBlock == null)
BlockData parentBlockData = RepositoryManager.getBlockRepository().fromSignature(this.blockData.getReference());
if (parentBlockData == null)
return false;
Block parentBlock = new Block(parentBlockData);
// Check timestamp is valid, i.e. later than parent timestamp and not in the future, within ~500ms margin
if (this.timestamp < parentBlock.getTimestamp() || this.timestamp - BLOCK_TIMESTAMP_MARGIN > NTP.getTime())
if (this.blockData.getTimestamp() < parentBlockData.getTimestamp() || this.blockData.getTimestamp() - BLOCK_TIMESTAMP_MARGIN > NTP.getTime())
return false;
// Legacy gen1 test: check timestamp ms is the same as parent timestamp ms?
if (this.timestamp % 1000 != parentBlock.getTimestamp() % 1000)
if (this.blockData.getTimestamp() % 1000 != parentBlockData.getTimestamp() % 1000)
return false;
// Check block version
if (this.version != parentBlock.getNextBlockVersion())
if (this.blockData.getVersion() != parentBlock.getNextBlockVersion())
return false;
if (this.version < 2 && (this.atBytes != null || this.atBytes.length > 0 || this.atFees != null || this.atFees.compareTo(BigDecimal.ZERO) > 0))
if (this.blockData.getVersion() < 2 && (this.blockData.getAtBytes() != null || this.blockData.getAtFees() != null))
return false;
// Check generating balance
if (this.generatingBalance != parentBlock.getNextBlockGeneratingBalance())
if (this.blockData.getGeneratingBalance() != parentBlock.getNextBlockGeneratingBalance())
return false;
// Check generator's proof of stake against block's generating balance
// TODO
// Check CIYAM AT
if (this.atBytes != null && this.atBytes.length > 0) {
if (this.blockData.getAtBytes() != null && this.blockData.getAtBytes().length > 0) {
// TODO
// try {
// AT_Block atBlock = AT_Controller.validateATs(this.getBlockATs(), BlockChain.getHeight() + 1);
@ -796,18 +389,18 @@ public class Block {
// Check transactions
Savepoint savepoint = DB.createSavepoint("BLOCK_TRANSACTIONS");
try {
for (TransactionHandler transaction : this.getTransactions()) {
for (Transaction transaction : this.getTransactions()) {
// GenesisTransactions are not allowed (GenesisBlock overrides isValid() to allow them)
if (transaction instanceof GenesisTransaction)
return false;
// Check timestamp and deadline
if (transaction.getTimestamp() > this.timestamp || transaction.getDeadline() <= this.timestamp)
if (transaction.getTransactionData().getTimestamp() > this.blockData.getTimestamp() || transaction.getDeadline() <= this.blockData.getTimestamp())
return false;
// Check transaction is even valid
// NOTE: in Gen1 there was an extra block height passed to DeployATTransaction.isValid
if (transaction.isValid() != TransactionHandler.ValidationResult.OK)
if (transaction.isValid() != Transaction.ValidationResult.OK)
return false;
// Process transaction to make sure other transactions validate properly
@ -818,6 +411,8 @@ public class Block {
return false;
}
}
} catch (DataException e) {
return false;
} finally {
// Revert back to savepoint
try {
@ -834,32 +429,32 @@ public class Block {
return true;
}
public void process() throws SQLException {
public void process() throws DataException, SQLException {
// Process transactions (we'll link them to this block after saving the block itself)
List<TransactionHandler> transactions = this.getTransactions();
for (TransactionHandler transaction : transactions)
List<Transaction> transactions = this.getTransactions();
for (Transaction transaction : transactions)
transaction.process();
// If fees are non-zero then add fees to generator's balance
BigDecimal blockFee = this.getTotalFees();
BigDecimal blockFee = this.blockData.getTotalFees();
if (blockFee.compareTo(BigDecimal.ZERO) == 1)
this.generator.setConfirmedBalance(Asset.QORA, this.generator.getConfirmedBalance(Asset.QORA).add(blockFee));
// Link block into blockchain by fetching signature of highest block and setting that as our reference
int blockchainHeight = BlockChain.getHeight();
Block latestBlock = Block.fromHeight(blockchainHeight);
if (latestBlock != null)
this.reference = latestBlock.getSignature();
BlockData latestBlockData = RepositoryManager.getBlockRepository().fromHeight(blockchainHeight);
if (latestBlockData != null)
this.blockData.setReference(latestBlockData.getSignature());
this.height = blockchainHeight + 1;
this.save();
this.blockData.setHeight(blockchainHeight + 1);
RepositoryManager.getBlockRepository().save(this.blockData);
// Link transactions to this block, thus removing them from unconfirmed transactions list.
for (int sequence = 0; sequence < transactions.size(); ++sequence) {
TransactionHandler transaction = transactions.get(sequence);
Transaction transaction = transactions.get(sequence);
// Link transaction to this block
BlockTransaction blockTransaction = new BlockTransaction(this.getSignature(), sequence, transaction.getSignature());
BlockTransaction blockTransaction = new BlockTransaction(this.getSignature(), sequence, transaction.getTransactionData().getSignature());
blockTransaction.save();
}
}

View File

@ -87,14 +87,17 @@ public class BlockChain {
* Return highest block height from DB.
*
* @return height, or 0 if there are no blocks in DB (not very likely).
* @throws SQLException
*/
public static int getHeight() throws SQLException {
ResultSet rs = DB.checkedExecute("SELECT MAX(height) FROM Blocks");
if (rs == null)
return 0;
public static int getHeight() {
try {
ResultSet rs = DB.checkedExecute("SELECT MAX(height) FROM Blocks");
if (rs == null)
return 0;
return rs.getInt(1);
return rs.getInt(1);
} catch (SQLException e) {
return 0;
}
}
/**

View File

@ -5,9 +5,8 @@ import java.sql.SQLException;
import database.DB;
import database.NoDataFoundException;
import qora.transaction.TransactionHandler;
import qora.transaction.Transaction;
import repository.hsqldb.HSQLDBSaver;
import qora.transaction.TransactionHandler;
public class BlockTransaction {
@ -50,7 +49,7 @@ public class BlockTransaction {
this.blockSignature = blockSignature;
this.sequence = sequence;
this.transactionSignature = DB.getResultSetBytes(rs.getBinaryStream(1), TransactionHandler.SIGNATURE_LENGTH);
this.transactionSignature = DB.getResultSetBytes(rs.getBinaryStream(1));
}
protected BlockTransaction(byte[] transactionSignature) throws SQLException {
@ -58,7 +57,7 @@ public class BlockTransaction {
if (rs == null)
throw new NoDataFoundException();
this.blockSignature = DB.getResultSetBytes(rs.getBinaryStream(1), Block.BLOCK_SIGNATURE_LENGTH);
this.blockSignature = DB.getResultSetBytes(rs.getBinaryStream(1));
this.sequence = rs.getInt(2);
this.transactionSignature = transactionSignature;
}
@ -118,8 +117,10 @@ public class BlockTransaction {
* @return Transaction, or null if not found (which should never happen)
* @throws SQLException
*/
public TransactionHandler getTransaction() throws SQLException {
return TransactionFactory.fromSignature(this.transactionSignature);
public Transaction getTransaction() throws SQLException {
// XXX
// return TransactionFactory.fromSignature(this.transactionSignature);
return null;
}
}

View File

@ -1,151 +1,21 @@
package qora.transaction;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.math.BigDecimal;
import java.nio.ByteBuffer;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Arrays;
import org.json.simple.JSONObject;
import com.google.common.primitives.Bytes;
import com.google.common.primitives.Ints;
import com.google.common.primitives.Longs;
import database.DB;
import database.NoDataFoundException;
import qora.account.Account;
import qora.account.GenesisAccount;
import data.transaction.GenesisTransactionData;
import data.transaction.TransactionData;
import qora.account.PrivateKeyAccount;
import qora.assets.Asset;
import qora.crypto.Crypto;
import repository.hsqldb.HSQLDBSaver;
import transform.TransformationException;
import utils.Base58;
import utils.Serialization;
import transform.transaction.TransactionTransformer;
public class GenesisTransaction extends TransactionHandler {
public class GenesisTransaction extends Transaction {
// Properties
private Account recipient;
private BigDecimal amount;
// Property lengths
private static final int RECIPIENT_LENGTH = 25; // raw, not Base58-encoded
private static final int AMOUNT_LENGTH = 8;
// Note that Genesis transactions don't require reference, fee or signature:
private static final int TYPELESS_LENGTH = TIMESTAMP_LENGTH + RECIPIENT_LENGTH + AMOUNT_LENGTH;
// Constructors
public GenesisTransaction(String recipient, BigDecimal amount, long timestamp) {
super(TransactionType.GENESIS, BigDecimal.ZERO, new GenesisAccount(), timestamp, null, null);
this.recipient = new Account(recipient);
this.amount = amount;
this.signature = calcSignature();
}
// Getters/Setters
public Account getRecipient() {
return this.recipient;
}
public BigDecimal getAmount() {
return this.amount;
}
// More information
@Override
public int getDataLength() {
return TYPE_LENGTH + TYPELESS_LENGTH;
}
// Load/Save
/**
* Load GenesisTransaction from DB using signature.
*
* @param signature
* @throws NoDataFoundException
* if no matching row found
* @throws SQLException
*/
protected GenesisTransaction(byte[] signature) throws SQLException {
super(TransactionType.GENESIS, signature);
ResultSet rs = DB.checkedExecute("SELECT recipient, amount FROM GenesisTransactions WHERE signature = ?", signature);
if (rs == null)
throw new NoDataFoundException();
this.recipient = new Account(rs.getString(1));
this.amount = rs.getBigDecimal(2).setScale(8);
}
/**
* Load GenesisTransaction from DB using signature
*
* @param signature
* @return GenesisTransaction, or null if not found
* @throws SQLException
*/
public static GenesisTransaction fromSignature(byte[] signature) throws SQLException {
try {
return new GenesisTransaction(signature);
} catch (NoDataFoundException e) {
return null;
}
}
@Override
public void save() throws SQLException {
super.save();
HSQLDBSaver saveHelper = new HSQLDBSaver("GenesisTransactions");
saveHelper.bind("signature", this.signature).bind("recipient", this.recipient.getAddress()).bind("amount", this.amount);
saveHelper.execute();
}
// Converters
protected static TransactionHandler parse(ByteBuffer byteBuffer) throws TransformationException {
if (byteBuffer.remaining() < TYPELESS_LENGTH)
throw new TransformationException("Byte data too short for GenesisTransaction");
long timestamp = byteBuffer.getLong();
String recipient = Serialization.deserializeRecipient(byteBuffer);
BigDecimal amount = Serialization.deserializeBigDecimal(byteBuffer);
return new GenesisTransaction(recipient, amount, timestamp);
}
@SuppressWarnings("unchecked")
@Override
public JSONObject toJSON() throws SQLException {
JSONObject json = getBaseJSON();
json.put("recipient", this.recipient.getAddress());
json.put("amount", this.amount.toPlainString());
return json;
}
@Override
public byte[] toBytes() {
try {
ByteArrayOutputStream bytes = new ByteArrayOutputStream(getDataLength());
bytes.write(Ints.toByteArray(this.type.value));
bytes.write(Longs.toByteArray(this.timestamp));
bytes.write(Base58.decode(this.recipient.getAddress()));
bytes.write(Serialization.serializeBigDecimal(this.amount));
return bytes.toByteArray();
} catch (IOException e) {
throw new RuntimeException(e);
}
public GenesisTransaction(TransactionData transactionData) {
this.transactionData = transactionData;
}
// Processing
@ -174,8 +44,12 @@ public class GenesisTransaction extends TransactionHandler {
* @return byte[]
*/
private byte[] calcSignature() {
byte[] digest = Crypto.digest(toBytes());
return Bytes.concat(digest, digest);
try {
byte[] digest = Crypto.digest(TransactionTransformer.toBytes(this.transactionData));
return Bytes.concat(digest, digest);
} catch (TransformationException e) {
return null;
}
}
/**
@ -189,42 +63,50 @@ public class GenesisTransaction extends TransactionHandler {
*/
@Override
public boolean isSignatureValid() {
return Arrays.equals(this.signature, calcSignature());
return Arrays.equals(this.transactionData.getSignature(), this.calcSignature());
}
@Override
public ValidationResult isValid() {
GenesisTransactionData genesisTransaction = (GenesisTransactionData) this.transactionData;
// Check amount is zero or positive
if (this.amount.compareTo(BigDecimal.ZERO) == -1)
if (genesisTransaction.getAmount().compareTo(BigDecimal.ZERO) == -1)
return ValidationResult.NEGATIVE_AMOUNT;
// Check recipient address is valid
if (!Crypto.isValidAddress(this.recipient.getAddress()))
if (!Crypto.isValidAddress(genesisTransaction.getRecipient()))
return ValidationResult.INVALID_ADDRESS;
return ValidationResult.OK;
}
@Override
public void process() throws SQLException {
this.save();
public void process() {
// TODO
// this.save();
// Set recipient's balance
this.recipient.setConfirmedBalance(Asset.QORA, this.amount);
// TODO
// this.recipient.setConfirmedBalance(Asset.QORA, this.amount);
// Set recipient's reference
recipient.setLastReference(this.signature);
// TODO
// recipient.setLastReference(this.signature);
}
@Override
public void orphan() throws SQLException {
this.delete();
public void orphan() {
// TODO
// this.delete();
// Reset recipient's balance
this.recipient.deleteBalance(Asset.QORA);
// TODO
// this.recipient.deleteBalance(Asset.QORA);
// Set recipient's reference
recipient.setLastReference(null);
// TODO
// recipient.setLastReference(null);
}
}

View File

@ -7,25 +7,36 @@ import java.util.Map;
import static java.util.Arrays.stream;
import static java.util.stream.Collectors.toMap;
import org.json.simple.JSONObject;
import data.transaction.Transaction;
import database.DB;
import database.NoDataFoundException;
import data.block.BlockData;
import data.transaction.TransactionData;
import qora.account.PrivateKeyAccount;
import qora.account.PublicKeyAccount;
import qora.block.Block;
import qora.block.BlockChain;
import qora.block.BlockTransaction;
import repository.Repository;
import repository.RepositoryManager;
import settings.Settings;
import transform.TransformationException;
import transform.Transformer;
import transform.transaction.TransactionTransformer;
import utils.Base58;
public abstract class TransactionHandler {
public abstract class Transaction {
// Transaction types
public enum TransactionType {
GENESIS(1), PAYMENT(2), REGISTER_NAME(3), UPDATE_NAME(4), SELL_NAME(5), CANCEL_SELL_NAME(6), BUY_NAME(7), CREATE_POLL(8), VOTE_ON_POLL(9), ARBITRARY(
10), ISSUE_ASSET(11), TRANSFER_ASSET(12), CREATE_ASSET_ORDER(13), CANCEL_ASSET_ORDER(14), MULTIPAYMENT(15), DEPLOY_AT(16), MESSAGE(17);
public final int value;
private final static Map<Integer, TransactionType> map = stream(TransactionType.values()).collect(toMap(type -> type.value, type -> type));
TransactionType(int value) {
this.value = value;
}
public static TransactionType valueOf(int value) {
return map.get(value);
}
}
// Validation results
public enum ValidationResult {
@ -52,30 +63,40 @@ public abstract class TransactionHandler {
protected static final BigDecimal maxBytePerFee = BigDecimal.valueOf(Settings.getInstance().getMaxBytePerFee());
protected static final BigDecimal minFeePerByte = BigDecimal.ONE.divide(maxBytePerFee, MathContext.DECIMAL32);
private Transaction transaction;
protected TransactionData transactionData;
// Constructors
public TransactionHandler(Transaction transaction) {
this.transaction = transaction;
public static Transaction fromData(TransactionData transactionData) {
switch (transactionData.getType()) {
case GENESIS:
return new GenesisTransaction(transactionData);
default:
return null;
}
}
// Getters / Setters
public TransactionData getTransactionData() {
return this.transactionData;
}
// More information
public long getDeadline() {
// 24 hour deadline to include transaction in a block
return this.transaction.getTimestamp() + (24 * 60 * 60 * 1000);
return this.transactionData.getTimestamp() + (24 * 60 * 60 * 1000);
}
public boolean hasMinimumFee() {
return this.transaction.getFee().compareTo(MINIMUM_FEE) >= 0;
return this.transactionData.getFee().compareTo(MINIMUM_FEE) >= 0;
}
public BigDecimal feePerByte() {
try {
return this.transaction.getFee().divide(new BigDecimal(TransactionTransformer.getDataLength(this.transaction)), MathContext.DECIMAL32);
return this.transactionData.getFee().divide(new BigDecimal(TransactionTransformer.getDataLength(this.transactionData)), MathContext.DECIMAL32);
} catch (TransformationException e) {
throw new IllegalStateException("Unable to get transaction byte length?");
}
@ -87,7 +108,7 @@ public abstract class TransactionHandler {
public BigDecimal calcRecommendedFee() {
try {
BigDecimal recommendedFee = BigDecimal.valueOf(TransactionTransformer.getDataLength(this.transaction)).divide(maxBytePerFee, MathContext.DECIMAL32).setScale(8);
BigDecimal recommendedFee = BigDecimal.valueOf(TransactionTransformer.getDataLength(this.transactionData)).divide(maxBytePerFee, MathContext.DECIMAL32).setScale(8);
// security margin
recommendedFee = recommendedFee.add(new BigDecimal("0.0000001"));
@ -118,7 +139,7 @@ public abstract class TransactionHandler {
* @return height, or 0 if not in blockchain (i.e. unconfirmed)
*/
public int getHeight() {
return RepositoryManager.getTransactionRepository().getHeight(this.transaction);
return RepositoryManager.getTransactionRepository().getHeight(this.transactionData);
}
/**
@ -132,6 +153,9 @@ public abstract class TransactionHandler {
return 0;
int blockChainHeight = BlockChain.getHeight();
if (blockChainHeight == 0)
return 0;
return blockChainHeight - ourHeight + 1;
}
@ -142,8 +166,8 @@ public abstract class TransactionHandler {
*
* @return Block, or null if transaction is not in a Block
*/
public Block getBlock() {
return RepositoryManager.getTransactionRepository().toBlock(this.transaction);
public BlockData getBlock() {
return RepositoryManager.getTransactionRepository().toBlock(this.transactionData);
}
/**
@ -151,8 +175,8 @@ public abstract class TransactionHandler {
*
* @return Transaction, or null if no parent found (which should not happen)
*/
public Transaction getParent() {
byte[] reference = this.transaction.getReference();
public TransactionData getParent() {
byte[] reference = this.transactionData.getReference();
if (reference == null)
return null;
@ -164,8 +188,8 @@ public abstract class TransactionHandler {
*
* @return Transaction, or null if no child found
*/
public Transaction getChild() {
byte[] signature = this.transaction.getSignature();
public TransactionData getChild() {
byte[] signature = this.transactionData.getSignature();
if (signature == null)
return null;
@ -181,7 +205,7 @@ public abstract class TransactionHandler {
*/
private byte[] toBytesLessSignature() {
try {
byte[] bytes = TransactionTransformer.toBytes(this.transaction);
byte[] bytes = TransactionTransformer.toBytes(this.transactionData);
return Arrays.copyOf(bytes, bytes.length - Transformer.SIGNATURE_LENGTH);
} catch (TransformationException e) {
// XXX this isn't good
@ -196,7 +220,7 @@ public abstract class TransactionHandler {
}
public boolean isSignatureValid() {
byte[] signature = this.transaction.getSignature();
byte[] signature = this.transactionData.getSignature();
if (signature == null)
return false;

View File

@ -1,8 +1,20 @@
package repository;
import java.util.List;
import data.block.BlockData;
import data.transaction.TransactionData;
public interface BlockRepository {
BlockData fromSignature(byte[] signature) throws DataException;
BlockData fromHeight(int height) throws DataException;
public BlockData fromSignature(byte[] signature) throws DataException;
public BlockData fromReference(byte[] reference) throws DataException;
public BlockData fromHeight(int height) throws DataException;
public List<TransactionData> getTransactionsFromSignature(byte[] signature) throws DataException;
public void save(BlockData blockData) throws DataException;
}

View File

@ -1,20 +1,20 @@
package repository;
import data.transaction.Transaction;
import qora.block.Block;
import data.transaction.TransactionData;
import data.block.BlockData;
public interface TransactionRepository {
public Transaction fromSignature(byte[] signature);
public TransactionData fromSignature(byte[] signature);
public Transaction fromReference(byte[] reference);
public TransactionData fromReference(byte[] reference);
public int getHeight(Transaction transaction);
public int getHeight(TransactionData transaction);
public Block toBlock(Transaction transaction);
public BlockData toBlock(TransactionData transaction);
public void save(Transaction transaction);
public void save(TransactionData transaction) throws DataException;
public void delete(Transaction transaction);
public void delete(TransactionData transaction) throws DataException;
}

View File

@ -1,20 +1,21 @@
package repository.hsqldb;
import java.math.BigDecimal;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Timestamp;
import java.util.ArrayList;
import java.util.List;
import data.block.Block;
import data.block.BlockData;
import data.transaction.TransactionData;
import database.DB;
import qora.account.PublicKeyAccount;
import repository.BlockRepository;
import repository.DataException;
import repository.RepositoryManager;
import repository.TransactionRepository;
public class HSQLDBBlockRepository implements BlockRepository
{
public class HSQLDBBlockRepository implements BlockRepository {
protected static final int TRANSACTIONS_SIGNATURE_LENGTH = 64;
protected static final int GENERATOR_SIGNATURE_LENGTH = 64;
protected static final int REFERENCE_LENGTH = GENERATOR_SIGNATURE_LENGTH + TRANSACTIONS_SIGNATURE_LENGTH;
@ -27,30 +28,38 @@ public class HSQLDBBlockRepository implements BlockRepository
public HSQLDBBlockRepository(HSQLDBRepository repository) {
this.repository = repository;
}
public BlockData fromSignature(byte[] signature) throws DataException
{
ResultSet rs;
public BlockData fromSignature(byte[] signature) throws DataException {
try {
rs = DB.checkedExecute(repository.connection, "SELECT " + BLOCK_DB_COLUMNS + " FROM Blocks WHERE signature = ?", signature);
ResultSet rs = DB.checkedExecute(repository.connection, "SELECT " + BLOCK_DB_COLUMNS + " FROM Blocks WHERE signature = ?", signature);
return getBlockFromResultSet(rs);
} catch (SQLException e) {
throw new DataException("Error loading data from DB", e);
}
return getBlockFromResultSet(rs);
}
public BlockData fromHeight(int height) throws DataException
{
ResultSet rs;
public BlockData fromReference(byte[] reference) throws DataException {
try {
rs = DB.checkedExecute(repository.connection, "SELECT " + BLOCK_DB_COLUMNS + " FROM Blocks WHERE height = ?", height);
ResultSet rs = DB.checkedExecute(repository.connection, "SELECT " + BLOCK_DB_COLUMNS + " FROM Blocks WHERE height = ?", height);
return getBlockFromResultSet(rs);
} catch (SQLException e) {
throw new DataException("Error loading data from DB", e);
}
}
public BlockData fromHeight(int height) throws DataException {
try {
ResultSet rs = DB.checkedExecute(repository.connection, "SELECT " + BLOCK_DB_COLUMNS + " FROM Blocks WHERE height = ?", height);
return getBlockFromResultSet(rs);
} catch (SQLException e) {
throw new DataException("Error loading data from DB", e);
}
return getBlockFromResultSet(rs);
}
private BlockData getBlockFromResultSet(ResultSet rs) throws DataException {
if (rs == null)
return null;
try {
int version = rs.getInt(1);
byte[] reference = DB.getResultSetBytes(rs.getBinaryStream(2));
@ -65,12 +74,49 @@ public class HSQLDBBlockRepository implements BlockRepository
byte[] atBytes = DB.getResultSetBytes(rs.getBinaryStream(11));
BigDecimal atFees = rs.getBigDecimal(12);
return new Block(version, reference, transactionCount, totalFees, transactionsSignature, height, timestamp,
generatingBalance,generatorPublicKey, generatorSignature, atBytes, atFees);
}
catch(SQLException e)
{
return new BlockData(version, reference, transactionCount, totalFees, transactionsSignature, height, timestamp, generatingBalance, generatorPublicKey,
generatorSignature, atBytes, atFees);
} catch(SQLException e) {
throw new DataException("Error extracting data from result set", e);
}
}
public List<TransactionData> getTransactionsFromSignature(byte[] signature) throws DataException {
List<TransactionData> transactions = new ArrayList<TransactionData>();
try {
ResultSet rs = DB.checkedExecute("SELECT transaction_signature FROM BlockTransactions WHERE block_signature = ?", signature);
if (rs == null)
return transactions; // No transactions in this block
TransactionRepository transactionRepo = RepositoryManager.getTransactionRepository();
// NB: do-while loop because DB.checkedExecute() implicitly calls ResultSet.next() for us
do {
byte[] transactionSignature = DB.getResultSetBytes(rs.getBinaryStream(1));
transactions.add(transactionRepo.fromSignature(transactionSignature));
} while (rs.next());
} catch (SQLException e) {
throw new DataException(e);
}
return transactions;
}
public void save(BlockData blockData) throws DataException {
HSQLDBSaver saveHelper = new HSQLDBSaver("Blocks");
saveHelper.bind("signature", blockData.getSignature()).bind("version", blockData.getVersion()).bind("reference", blockData.getReference())
.bind("transaction_count", blockData.getTransactionCount()).bind("total_fees", blockData.getTotalFees()).bind("transactions_signature", blockData.getTransactionsSignature())
.bind("height", blockData.getHeight()).bind("generation", new Timestamp(blockData.getTimestamp())).bind("generating_balance", blockData.getGeneratingBalance())
.bind("generator", blockData.getGeneratorPublicKey()).bind("generator_signature", blockData.getGeneratorSignature()).bind("AT_data", blockData.getAtBytes())
.bind("AT_fees", blockData.getAtFees());
try {
saveHelper.execute();
} catch (SQLException e) {
throw new DataException("Unable to save Block into repository", e);
}
}
}

View File

@ -4,11 +4,10 @@ import java.math.BigDecimal;
import java.sql.ResultSet;
import java.sql.SQLException;
import data.account.Account;
import data.account.PublicKeyAccount;
import data.transaction.GenesisTransaction;
import data.transaction.Transaction;
import data.transaction.GenesisTransactionData;
import data.transaction.TransactionData;
import database.DB;
import repository.DataException;
public class HSQLDBGenesisTransactionRepository extends HSQLDBTransactionRepository {
@ -16,19 +15,34 @@ public class HSQLDBGenesisTransactionRepository extends HSQLDBTransactionReposit
super(repository);
}
Transaction fromBase(byte[] signature, byte[] reference, PublicKeyAccount creator, long timestamp, BigDecimal fee) {
TransactionData fromBase(byte[] signature, byte[] reference, byte[] creator, long timestamp, BigDecimal fee) {
try {
ResultSet rs = DB.checkedExecute(repository.connection, "SELECT recipient, amount FROM GenesisTransactions WHERE signature = ?", signature);
if (rs == null)
return null;
Account recipient = new Account(rs.getString(1));
String recipient = rs.getString(1);
BigDecimal amount = rs.getBigDecimal(2).setScale(8);
return new GenesisTransaction(recipient, amount, timestamp, signature);
return new GenesisTransactionData(recipient, amount, timestamp, signature);
} catch (SQLException e) {
return null;
}
}
@Override
public void save(TransactionData transaction) throws DataException {
super.save(transaction);
GenesisTransactionData genesisTransaction = (GenesisTransactionData) transaction;
HSQLDBSaver saveHelper = new HSQLDBSaver("GenesisTransactions");
saveHelper.bind("signature", genesisTransaction.getSignature()).bind("recipient", genesisTransaction.getRecipient()).bind("amount", genesisTransaction.getAmount());
try {
saveHelper.execute();
} catch (SQLException e) {
throw new DataException(e);
}
}
}

View File

@ -6,11 +6,12 @@ import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Timestamp;
import data.account.PublicKeyAccount;
import data.transaction.Transaction;
import data.transaction.Transaction.TransactionType;
import data.block.BlockData;
import data.transaction.TransactionData;
import qora.transaction.Transaction.TransactionType;
import database.DB;
import qora.block.Block;
import repository.DataException;
import repository.RepositoryManager;
import repository.TransactionRepository;
public class HSQLDBTransactionRepository implements TransactionRepository {
@ -23,7 +24,7 @@ public class HSQLDBTransactionRepository implements TransactionRepository {
genesisTransactionRepository = new HSQLDBGenesisTransactionRepository(repository);
}
public Transaction fromSignature(byte[] signature) {
public TransactionData fromSignature(byte[] signature) {
try {
ResultSet rs = DB.checkedExecute(repository.connection, "SELECT type, reference, creator, creation, fee FROM Transactions WHERE signature = ?", signature);
if (rs == null)
@ -31,7 +32,7 @@ public class HSQLDBTransactionRepository implements TransactionRepository {
TransactionType type = TransactionType.valueOf(rs.getInt(1));
byte[] reference = DB.getResultSetBytes(rs.getBinaryStream(2));
PublicKeyAccount creator = new PublicKeyAccount(DB.getResultSetBytes(rs.getBinaryStream(3)));
byte[] creator = DB.getResultSetBytes(rs.getBinaryStream(3));
long timestamp = rs.getTimestamp(4).getTime();
BigDecimal fee = rs.getBigDecimal(5).setScale(8);
@ -41,7 +42,7 @@ public class HSQLDBTransactionRepository implements TransactionRepository {
}
}
public Transaction fromReference(byte[] reference) {
public TransactionData fromReference(byte[] reference) {
try {
ResultSet rs = DB.checkedExecute(repository.connection, "SELECT type, signature, creator, creation, fee FROM Transactions WHERE reference = ?", reference);
if (rs == null)
@ -49,7 +50,7 @@ public class HSQLDBTransactionRepository implements TransactionRepository {
TransactionType type = TransactionType.valueOf(rs.getInt(1));
byte[] signature = DB.getResultSetBytes(rs.getBinaryStream(2));
PublicKeyAccount creator = new PublicKeyAccount(DB.getResultSetBytes(rs.getBinaryStream(3)));
byte[] creator = DB.getResultSetBytes(rs.getBinaryStream(3));
long timestamp = rs.getTimestamp(4).getTime();
BigDecimal fee = rs.getBigDecimal(5).setScale(8);
@ -59,7 +60,7 @@ public class HSQLDBTransactionRepository implements TransactionRepository {
}
}
private Transaction fromBase(TransactionType type, byte[] signature, byte[] reference, PublicKeyAccount creator, long timestamp, BigDecimal fee) {
private TransactionData fromBase(TransactionType type, byte[] signature, byte[] reference, byte[] creator, long timestamp, BigDecimal fee) {
switch (type) {
case GENESIS:
return this.genesisTransactionRepository.fromBase(signature, reference, creator, timestamp, fee);
@ -70,26 +71,27 @@ public class HSQLDBTransactionRepository implements TransactionRepository {
}
@Override
public int getHeight(Transaction transaction) {
byte[] signature = transaction.getSignature();
public int getHeight(TransactionData transactionData) {
byte[] signature = transactionData.getSignature();
if (signature == null)
return 0;
// in one go?
try {
ResultSet rs = DB.checkedExecute(repository.connection, "SELECT height from BlockTransactions JOIN Blocks ON Blocks.signature = BlockTransactions.block_signature WHERE transaction_signature = ? LIMIT 1", signature);
if (rs == null)
return 0;
return rs.getInt(1);
} catch (SQLException e) {
return 0;
}
}
@Override
public Block toBlock(Transaction transaction) {
byte[] signature = transaction.getSignature();
public BlockData toBlock(TransactionData transactionData) {
byte[] signature = transactionData.getSignature();
if (signature == null)
return null;
@ -98,33 +100,30 @@ public class HSQLDBTransactionRepository implements TransactionRepository {
ResultSet rs = DB.checkedExecute(repository.connection, "SELECT block_signature from BlockTransactions WHERE transaction_signature = ? LIMIT 1", signature);
if (rs == null)
return null;
byte[] blockSignature = DB.getResultSetBytes(rs.getBinaryStream(1));
// TODO
// return RepositoryManager.getBlockRepository().fromSignature(blockSignature);
return null;
} catch (SQLException e) {
return RepositoryManager.getBlockRepository().fromSignature(blockSignature);
} catch (SQLException | DataException e) {
return null;
}
}
@Override
public void save(Transaction transaction) {
public void save(TransactionData transactionData) throws DataException {
HSQLDBSaver saver = new HSQLDBSaver("Transactions");
saver.bind("signature", transaction.getSignature()).bind("reference", transaction.getReference()).bind("type", transaction.getType().value)
.bind("creator", transaction.getCreator().getPublicKey()).bind("creation", new Timestamp(transaction.getTimestamp())).bind("fee", transaction.getFee())
saver.bind("signature", transactionData.getSignature()).bind("reference", transactionData.getReference()).bind("type", transactionData.getType().value)
.bind("creator", transactionData.getCreatorPublicKey()).bind("creation", new Timestamp(transactionData.getTimestamp())).bind("fee", transactionData.getFee())
.bind("milestone_block", null);
try {
saver.execute(repository.connection);
} catch (SQLException e) {
// XXX do what?
throw new DataException(e);
}
}
@Override
public void delete(Transaction transaction) {
public void delete(TransactionData transactionData) {
// NOTE: The corresponding row in sub-table is deleted automatically by the database thanks to "ON DELETE CASCADE" in the sub-table's FOREIGN KEY
// definition.
try {

View File

@ -8,6 +8,7 @@ public abstract class Transformer {
// Raw, not Base58-encoded
public static final int ADDRESS_LENGTH = 25;
public static final int PUBLIC_KEY_LENGTH = 32;
public static final int SIGNATURE_LENGTH = 64;
public static final int TIMESTAMP_LENGTH = LONG_LENGTH;

View File

@ -0,0 +1,284 @@
package transform.block;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.math.BigDecimal;
import java.nio.ByteBuffer;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import org.json.simple.JSONArray;
import org.json.simple.JSONObject;
import com.google.common.hash.HashCode;
import com.google.common.primitives.Bytes;
import com.google.common.primitives.Ints;
import com.google.common.primitives.Longs;
import data.block.BlockData;
import data.transaction.TransactionData;
import qora.account.PublicKeyAccount;
import qora.block.Block;
import qora.transaction.Transaction;
import repository.DataException;
import transform.TransformationException;
import transform.Transformer;
import transform.transaction.TransactionTransformer;
import utils.Base58;
import utils.Serialization;
public class BlockTransformer extends Transformer {
private static final int VERSION_LENGTH = INT_LENGTH;
private static final int TRANSACTIONS_SIGNATURE_LENGTH = SIGNATURE_LENGTH;
private static final int GENERATOR_SIGNATURE_LENGTH = SIGNATURE_LENGTH;
private static final int BLOCK_REFERENCE_LENGTH = GENERATOR_SIGNATURE_LENGTH + TRANSACTIONS_SIGNATURE_LENGTH;
private static final int TIMESTAMP_LENGTH = LONG_LENGTH;
private static final int GENERATING_BALANCE_LENGTH = LONG_LENGTH;
private static final int GENERATOR_LENGTH = PUBLIC_KEY_LENGTH;
private static final int TRANSACTION_COUNT_LENGTH = INT_LENGTH;
private static final int BASE_LENGTH = VERSION_LENGTH + BLOCK_REFERENCE_LENGTH + TIMESTAMP_LENGTH + GENERATING_BALANCE_LENGTH + GENERATOR_LENGTH
+ TRANSACTIONS_SIGNATURE_LENGTH + GENERATOR_SIGNATURE_LENGTH + TRANSACTION_COUNT_LENGTH;
protected static final int BLOCK_SIGNATURE_LENGTH = GENERATOR_SIGNATURE_LENGTH + TRANSACTIONS_SIGNATURE_LENGTH;
protected static final int TRANSACTION_SIZE_LENGTH = INT_LENGTH; // per transaction
protected static final int AT_BYTES_LENGTH = INT_LENGTH;
protected static final int AT_FEES_LENGTH = LONG_LENGTH;
protected static final int AT_LENGTH = AT_FEES_LENGTH + AT_BYTES_LENGTH;
public static BlockData fromBytes(byte[] bytes) throws TransformationException {
if (bytes == null)
return null;
if (bytes.length < BASE_LENGTH)
throw new TransformationException("Byte data too short for Block");
ByteBuffer byteBuffer = ByteBuffer.wrap(bytes);
int version = byteBuffer.getInt();
if (version >= 2 && bytes.length < BASE_LENGTH + AT_LENGTH)
throw new TransformationException("Byte data too short for V2+ Block");
long timestamp = byteBuffer.getLong();
byte[] reference = new byte[BLOCK_REFERENCE_LENGTH];
byteBuffer.get(reference);
BigDecimal generatingBalance = BigDecimal.valueOf(byteBuffer.getLong()).setScale(8);
byte[] generatorPublicKey = Serialization.deserializePublicKey(byteBuffer);
byte[] transactionsSignature = new byte[TRANSACTIONS_SIGNATURE_LENGTH];
byteBuffer.get(transactionsSignature);
byte[] generatorSignature = new byte[GENERATOR_SIGNATURE_LENGTH];
byteBuffer.get(generatorSignature);
byte[] atBytes = null;
BigDecimal atFees = null;
if (version >= 2) {
int atBytesLength = byteBuffer.getInt();
if (atBytesLength > Block.MAX_BLOCK_BYTES)
throw new TransformationException("Byte data too long for Block's AT info");
atBytes = new byte[atBytesLength];
byteBuffer.get(atBytes);
atFees = BigDecimal.valueOf(byteBuffer.getLong()).setScale(8);
}
int transactionCount = byteBuffer.getInt();
// Parse transactions now, compared to deferred parsing in Gen1, so we can throw ParseException if need be
List<TransactionData> transactions = new ArrayList<TransactionData>();
for (int t = 0; t < transactionCount; ++t) {
if (byteBuffer.remaining() < TRANSACTION_SIZE_LENGTH)
throw new TransformationException("Byte data too short for Block Transaction length");
int transactionLength = byteBuffer.getInt();
if (byteBuffer.remaining() < transactionLength)
throw new TransformationException("Byte data too short for Block Transaction");
if (transactionLength > Block.MAX_BLOCK_BYTES)
throw new TransformationException("Byte data too long for Block Transaction");
byte[] transactionBytes = new byte[transactionLength];
byteBuffer.get(transactionBytes);
TransactionData transaction = TransactionTransformer.fromBytes(transactionBytes);
transactions.add(transaction);
}
if (byteBuffer.hasRemaining())
throw new TransformationException("Excess byte data found after parsing Block");
// XXX Can't return a simple BlockData object because it doesn't support holding the transactions
// return new BlockData(version, reference, timestamp, generatingBalance, generatorPublicKey, generatorSignature, transactionsSignature, atBytes, atFees, transactions);
return null;
}
public static int getDataLength(BlockData blockData) throws TransformationException {
// TODO
int blockLength = BASE_LENGTH;
if (blockData.getVersion() >= 2 && blockData.getAtBytes() != null)
blockLength += AT_FEES_LENGTH + AT_BYTES_LENGTH + blockData.getAtBytes().length;
/*
* XXX Where do the transactions come from? A param? Do we pass a Block instead of BlockData?
// Short cut for no transactions
if (block.getTransactions() == null || block.getTransactions().isEmpty())
return blockLength;
for (TransactionData transaction : this.transactions)
blockLength += TRANSACTION_SIZE_LENGTH + transaction.getDataLength();
*/
return blockLength;
}
public static byte[] toBytes(BlockData blockData) throws TransformationException {
try {
ByteArrayOutputStream bytes = new ByteArrayOutputStream(getDataLength(blockData));
bytes.write(Ints.toByteArray(blockData.getVersion()));
bytes.write(Longs.toByteArray(blockData.getTimestamp()));
bytes.write(blockData.getReference());
// NOTE: generatingBalance serialized as long value, not as BigDecimal, for historic compatibility
bytes.write(Longs.toByteArray(blockData.getGeneratingBalance().longValue()));
bytes.write(blockData.getGeneratorPublicKey());
bytes.write(blockData.getTransactionsSignature());
bytes.write(blockData.getGeneratorSignature());
if (blockData.getVersion() >= 2) {
byte[] atBytes = blockData.getAtBytes();
if (atBytes != null) {
bytes.write(Ints.toByteArray(atBytes.length));
bytes.write(atBytes);
// NOTE: atFees serialized as long value, not as BigDecimal, for historic compatibility
bytes.write(Longs.toByteArray(blockData.getAtFees().longValue()));
} else {
bytes.write(Ints.toByteArray(0));
bytes.write(Longs.toByteArray(0L));
}
}
// Transactions
bytes.write(Ints.toByteArray(blockData.getTransactionCount()));
/*
* XXX Where do the transactions come from? A param? Do we pass a Block instead of BlockData?
for (TransactionData transaction : blockData.getTransactions()) {
bytes.write(Ints.toByteArray(transaction.getDataLength()));
bytes.write(transaction.toBytes());
}
*/
return bytes.toByteArray();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
@SuppressWarnings("unchecked")
public static JSONObject toJSON(BlockData blockData) throws TransformationException {
JSONObject json = new JSONObject();
json.put("version", blockData.getVersion());
json.put("timestamp", blockData.getTimestamp());
json.put("generatingBalance", blockData.getGeneratingBalance());
json.put("generator", new PublicKeyAccount(blockData.getGeneratorPublicKey()).getAddress());
json.put("generatorPublicKey", Base58.encode(blockData.getGeneratorPublicKey()));
json.put("fee", blockData.getTotalFees().toPlainString());
json.put("transactionsSignature", Base58.encode(blockData.getTransactionsSignature()));
json.put("generatorSignature", Base58.encode(blockData.getGeneratorSignature()));
json.put("signature", Base58.encode(blockData.getSignature()));
if (blockData.getReference() != null)
json.put("reference", Base58.encode(blockData.getReference()));
json.put("height", blockData.getHeight());
// Add transaction info
JSONArray transactionsJson = new JSONArray();
boolean tradesHappened = false;
/*
* XXX Where do the transactions come from? A param? Do we pass a Block instead of BlockData?
for (TransactionData transaction : blockData.getTransactions()) {
transactionsJson.add(transaction.toJSON());
// If this is an asset CreateOrderTransaction then check to see if any trades happened
if (transaction.getType() == Transaction.TransactionType.CREATE_ASSET_ORDER) {
CreateOrderTransaction orderTransaction = (CreateOrderTransaction) transaction;
Order order = orderTransaction.getOrder();
List<Trade> trades = order.getTrades();
// Filter out trades with timestamps that don't match order transaction's timestamp
trades.removeIf((Trade trade) -> trade.getTimestamp() != order.getTimestamp());
// Any trades left?
if (!trades.isEmpty()) {
tradesHappened = true;
// No need to check any further
break;
}
}
}
json.put("transactions", transactionsJson);
*/
// Add asset trade activity flag
json.put("assetTrades", tradesHappened);
// Add CIYAM AT info (if any)
if (blockData.getAtBytes() != null) {
json.put("blockATs", HashCode.fromBytes(blockData.getAtBytes()).toString());
json.put("atFees", blockData.getAtFees());
}
return json;
}
public static byte[] getBytesForGeneratorSignature(BlockData blockData) throws TransformationException {
try {
ByteArrayOutputStream bytes = new ByteArrayOutputStream(GENERATOR_SIGNATURE_LENGTH + GENERATING_BALANCE_LENGTH + GENERATOR_LENGTH);
// Only copy the generator signature from reference, which is the first 64 bytes.
bytes.write(Arrays.copyOf(blockData.getReference(), GENERATOR_SIGNATURE_LENGTH));
bytes.write(Longs.toByteArray(blockData.getGeneratingBalance().longValue()));
// We're padding here just in case the generator is the genesis account whose public key is only 8 bytes long.
bytes.write(Bytes.ensureCapacity(blockData.getGeneratorPublicKey(), GENERATOR_LENGTH, 0));
return bytes.toByteArray();
} catch (IOException e) {
throw new TransformationException(e);
}
}
public static byte[] getBytesForTransactionsSignature(Block block) throws TransformationException {
ByteArrayOutputStream bytes = new ByteArrayOutputStream(GENERATOR_SIGNATURE_LENGTH + block.getBlockData().getTransactionCount() * TransactionTransformer.SIGNATURE_LENGTH);
try {
bytes.write(block.getBlockData().getGeneratorSignature());
for (Transaction transaction : block.getTransactions()) {
if (!transaction.isSignatureValid())
return null;
bytes.write(transaction.getTransactionData().getSignature());
}
return bytes.toByteArray();
} catch (IOException | DataException e) {
throw new TransformationException(e);
}
}
}

View File

@ -10,9 +10,8 @@ import org.json.simple.JSONObject;
import com.google.common.primitives.Ints;
import com.google.common.primitives.Longs;
import data.transaction.Transaction;
import data.account.Account;
import data.transaction.GenesisTransaction;
import data.transaction.TransactionData;
import data.transaction.GenesisTransactionData;
import transform.TransformationException;
import utils.Base58;
import utils.Serialization;
@ -25,30 +24,30 @@ public class GenesisTransactionTransformer extends TransactionTransformer {
// Note that Genesis transactions don't require reference, fee or signature:
private static final int TYPELESS_LENGTH = TIMESTAMP_LENGTH + RECIPIENT_LENGTH + AMOUNT_LENGTH;
static Transaction fromByteBuffer(ByteBuffer byteBuffer) throws TransformationException {
static TransactionData fromByteBuffer(ByteBuffer byteBuffer) throws TransformationException {
if (byteBuffer.remaining() < TYPELESS_LENGTH)
throw new TransformationException("Byte data too short for GenesisTransaction");
long timestamp = byteBuffer.getLong();
Account recipient = new Account(Serialization.deserializeRecipient(byteBuffer));
String recipient = Serialization.deserializeRecipient(byteBuffer);
BigDecimal amount = Serialization.deserializeBigDecimal(byteBuffer);
return new GenesisTransaction(recipient, amount, timestamp);
return new GenesisTransactionData(recipient, amount, timestamp);
}
public static int getDataLength(Transaction baseTransaction) throws TransformationException {
public static int getDataLength(TransactionData baseTransaction) throws TransformationException {
return TYPE_LENGTH + TYPELESS_LENGTH;
}
public static byte[] toBytes(Transaction baseTransaction) throws TransformationException {
public static byte[] toBytes(TransactionData baseTransaction) throws TransformationException {
try {
GenesisTransaction transaction = (GenesisTransaction) baseTransaction;
GenesisTransactionData transaction = (GenesisTransactionData) baseTransaction;
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
bytes.write(Ints.toByteArray(transaction.getType().value));
bytes.write(Longs.toByteArray(transaction.getTimestamp()));
bytes.write(Base58.decode(transaction.getRecipient().getAddress()));
bytes.write(Base58.decode(transaction.getRecipient()));
bytes.write(Serialization.serializeBigDecimal(transaction.getAmount()));
return bytes.toByteArray();
@ -58,13 +57,13 @@ public class GenesisTransactionTransformer extends TransactionTransformer {
}
@SuppressWarnings("unchecked")
public static JSONObject toJSON(Transaction baseTransaction) throws TransformationException {
public static JSONObject toJSON(TransactionData baseTransaction) throws TransformationException {
JSONObject json = TransactionTransformer.getBaseJSON(baseTransaction);
try {
GenesisTransaction transaction = (GenesisTransaction) baseTransaction;
GenesisTransactionData transaction = (GenesisTransactionData) baseTransaction;
json.put("recipient", transaction.getRecipient().getAddress());
json.put("recipient", transaction.getRecipient());
json.put("amount", transaction.getAmount().toPlainString());
} catch (ClassCastException e) {
throw new TransformationException(e);

View File

@ -4,8 +4,8 @@ import java.nio.ByteBuffer;
import org.json.simple.JSONObject;
import data.transaction.Transaction;
import data.transaction.Transaction.TransactionType;
import data.transaction.TransactionData;
import qora.transaction.Transaction.TransactionType;
import transform.TransformationException;
import transform.Transformer;
import utils.Base58;
@ -14,7 +14,7 @@ public class TransactionTransformer extends Transformer {
protected static final int TYPE_LENGTH = INT_LENGTH;
public static Transaction fromBytes(byte[] bytes) throws TransformationException {
public static TransactionData fromBytes(byte[] bytes) throws TransformationException {
if (bytes == null)
return null;
@ -36,7 +36,7 @@ public class TransactionTransformer extends Transformer {
}
}
public static int getDataLength(Transaction transaction) throws TransformationException {
public static int getDataLength(TransactionData transaction) throws TransformationException {
switch (transaction.getType()) {
case GENESIS:
return GenesisTransactionTransformer.getDataLength(transaction);
@ -46,7 +46,7 @@ public class TransactionTransformer extends Transformer {
}
}
public static byte[] toBytes(Transaction transaction) throws TransformationException {
public static byte[] toBytes(TransactionData transaction) throws TransformationException {
switch (transaction.getType()) {
case GENESIS:
return GenesisTransactionTransformer.toBytes(transaction);
@ -56,7 +56,7 @@ public class TransactionTransformer extends Transformer {
}
}
public static JSONObject toJSON(Transaction transaction) throws TransformationException {
public static JSONObject toJSON(TransactionData transaction) throws TransformationException {
switch (transaction.getType()) {
case GENESIS:
return GenesisTransactionTransformer.toJSON(transaction);
@ -67,7 +67,7 @@ public class TransactionTransformer extends Transformer {
}
@SuppressWarnings("unchecked")
static JSONObject getBaseJSON(Transaction transaction) {
static JSONObject getBaseJSON(TransactionData transaction) {
JSONObject json = new JSONObject();
json.put("type", transaction.getType().value);

View File

@ -6,8 +6,8 @@ import java.math.BigInteger;
import java.nio.ByteBuffer;
import qora.account.PublicKeyAccount;
import qora.transaction.TransactionHandler;
import transform.TransformationException;
import transform.Transformer;
public class Serialization {
@ -31,15 +31,15 @@ public class Serialization {
}
public static String deserializeRecipient(ByteBuffer byteBuffer) {
byte[] bytes = new byte[TransactionHandler.RECIPIENT_LENGTH];
byte[] bytes = new byte[Transformer.ADDRESS_LENGTH];
byteBuffer.get(bytes);
return Base58.encode(bytes);
}
public static PublicKeyAccount deserializePublicKey(ByteBuffer byteBuffer) {
byte[] bytes = new byte[TransactionHandler.CREATOR_LENGTH];
public static byte[] deserializePublicKey(ByteBuffer byteBuffer) {
byte[] bytes = new byte[Transformer.PUBLIC_KEY_LENGTH];
byteBuffer.get(bytes);
return new PublicKeyAccount(bytes);
return bytes;
}
public static String deserializeSizedString(ByteBuffer byteBuffer, int maxSize) throws TransformationException {