diff --git a/pom.xml b/pom.xml
index b44dacc8..078e0af6 100644
--- a/pom.xml
+++ b/pom.xml
@@ -42,5 +42,10 @@
@@ -237,4 +245,33 @@ public class DB { return resultSet.getLong(1); } + /** + * Efficiently query database for existing of matching row. + *
+ * {@code whereClause} is SQL "WHERE" clause containing "?" placeholders suitable for use with PreparedStatements. + *
+ * Example call: + *
+ * {@code String manufacturer = "Lamborghini";}
+ * Every BLOCK_RETARGET_INTERVAL the generating balance is recalculated.
+ *
+ * If this block starts a new interval then the new generating balance is calculated, cached and returned.
@@ -235,6 +339,10 @@ public class Block {
// 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");
+
return this.transactions;
}
@@ -406,7 +514,7 @@ public class Block {
return json;
}
- public byte[] toBytes() {
+ public byte[] toBytes() throws SQLException {
try {
ByteArrayOutputStream bytes = new ByteArrayOutputStream(getDataLength());
bytes.write(Ints.toByteArray(this.version));
@@ -430,6 +538,14 @@ public class Block {
}
}
+ // Transactions
+ bytes.write(Ints.toByteArray(this.transactionCount));
+
+ for (Transaction transaction : this.getTransactions()) {
+ bytes.write(Ints.toByteArray(transaction.getDataLength()));
+ bytes.write(transaction.toBytes());
+ }
+
return bytes.toByteArray();
} catch (IOException e) {
throw new RuntimeException(e);
@@ -437,70 +553,281 @@ public class Block {
}
public static Block parse(byte[] data) throws ParseException {
- // TODO
- return null;
+ if (data == null)
+ return null;
+
+ if (data.length < BASE_LENGTH)
+ throw new ParseException("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 ParseException("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 ParseException("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
+ * Used when constructing a new block during forging.
+ *
+ * Requires block's {@code generator} being a {@code PrivateKeyAccount} so block's transactions signature can be recalculated.
+ *
+ * @param transaction
+ * @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(Transaction transaction) {
- // TODO
+ // 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");
+
+ if (!(this.generator instanceof PrivateKeyAccount))
+ throw new IllegalStateException("Block's generator has no private key");
+
// Check there is space in block
+ if (this.getDataLength() + transaction.getDataLength() > MAX_BLOCK_BYTES)
+ return false;
+
// Add to block
+ this.transactions.add(transaction);
+
// Update transaction count
+ this.transactionCount++;
+
// Update totalFees
+ this.totalFees.add(transaction.getFee());
+
// Update transactions signature
- return false; // no room
+ calcTransactionsSignature();
+
+ return true;
}
- public byte[] calcSignature(PrivateKeyAccount signer) {
- // TODO
- return null;
+ /**
+ * Recalculate block's generator signature.
+ *
+ * Requires block's {@code generator} being a {@code PrivateKeyAccount}.
+ *
+ * @throws IllegalStateException
+ * if block's {@code generator} is not a {@code PrivateKeyAccount}.
+ */
+ 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[] getBytesForSignature() {
+ private byte[] getBytesForGeneratorSignature() {
try {
- ByteArrayOutputStream bytes = new ByteArrayOutputStream(REFERENCE_LENGTH + GENERATING_BALANCE_LENGTH + GENERATOR_LENGTH);
+ 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);
}
}
- public boolean isSignatureValid() {
- // Check generator's signature first
- if (!this.generator.verify(this.generatorSignature, getBytesForSignature()))
- return false;
+ /**
+ * Recalculate block's transactions signature.
+ *
+ * Requires block's {@code generator} being a {@code PrivateKeyAccount}.
+ *
+ * @throws IllegalStateException
+ * if block's {@code generator} is not a {@code PrivateKeyAccount}.
+ */
+ public void calcTransactionsSignature() {
+ if (!(this.generator instanceof PrivateKeyAccount))
+ throw new IllegalStateException("Block's generator has no private key");
- // Check transactions signature
+ this.transactionsSignature = ((PrivateKeyAccount) this.generator).sign(this.getBytesForTransactionsSignature());
+ }
+
+ private byte[] getBytesForTransactionsSignature() {
ByteArrayOutputStream bytes = new ByteArrayOutputStream(GENERATOR_SIGNATURE_LENGTH + this.transactionCount * Transaction.SIGNATURE_LENGTH);
+
try {
bytes.write(this.generatorSignature);
for (Transaction transaction : this.getTransactions()) {
if (!transaction.isSignatureValid())
- return false;
+ return null;
bytes.write(transaction.getSignature());
}
+
+ return bytes.toByteArray();
} catch (IOException | SQLException e) {
throw new RuntimeException(e);
}
+ }
- if (!this.generator.verify(this.transactionsSignature, bytes.toByteArray()))
+ 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()))
return false;
return true;
}
+ /**
+ * Returns whether Block is valid. Expected to be called within SQL Transaction.
+ *
+ * Performs various tests like checking for parent block, correct block timestamp, version, generating balance, etc.
* This is not possible as there is no private key for the genesis account and so no way to sign data.
*
@@ -255,8 +253,22 @@ public class GenesisBlock extends Block {
* @throws IllegalStateException
*/
@Override
- public byte[] calcSignature(PrivateKeyAccount signer) {
- throw new IllegalStateException("There is no private key for genesis transactions");
+ public void calcGeneratorSignature() {
+ throw new IllegalStateException("There is no private key for genesis account");
+ }
+
+ /**
+ * Refuse to calculate genesis block's transactions signature!
+ *
+ * This is not possible as there is no private key for the genesis account and so no way to sign data.
+ *
+ * Always throws IllegalStateException.
+ *
+ * @throws IllegalStateException
+ */
+ @Override
+ public void calcTransactionsSignature() {
+ throw new IllegalStateException("There is no private key for genesis account");
}
/**
diff --git a/src/qora/transaction/MessageTransaction.java b/src/qora/transaction/MessageTransaction.java
new file mode 100644
index 00000000..57350e80
--- /dev/null
+++ b/src/qora/transaction/MessageTransaction.java
@@ -0,0 +1,374 @@
+package qora.transaction;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.math.BigDecimal;
+import java.nio.ByteBuffer;
+import java.nio.charset.Charset;
+import java.sql.Connection;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.util.Arrays;
+
+import org.json.simple.JSONObject;
+
+import com.google.common.hash.HashCode;
+import com.google.common.primitives.Ints;
+import com.google.common.primitives.Longs;
+
+import database.DB;
+import database.NoDataFoundException;
+import database.SaveHelper;
+import qora.account.Account;
+import qora.account.PublicKeyAccount;
+import qora.assets.Asset;
+import qora.block.Block;
+import qora.block.BlockChain;
+import qora.crypto.Crypto;
+import utils.Base58;
+import utils.ParseException;
+import utils.Serialization;
+
+public class MessageTransaction extends Transaction {
+
+ // Properties
+ protected int version;
+ protected PublicKeyAccount sender;
+ protected Account recipient;
+ protected Long assetId;
+ protected BigDecimal amount;
+ protected byte[] data;
+ protected boolean isText;
+ protected boolean isEncrypted;
+
+ // Property lengths
+ private static final int SENDER_LENGTH = 32;
+ private static final int AMOUNT_LENGTH = 8;
+ private static final int ASSET_ID_LENGTH = 8;
+ private static final int DATA_SIZE_LENGTH = 4;
+ private static final int IS_TEXT_LENGTH = 1;
+ private static final int IS_ENCRYPTED_LENGTH = 1;
+ private static final int TYPELESS_DATALESS_LENGTH_V1 = BASE_TYPELESS_LENGTH + SENDER_LENGTH + RECIPIENT_LENGTH + AMOUNT_LENGTH + DATA_SIZE_LENGTH
+ + IS_TEXT_LENGTH + IS_ENCRYPTED_LENGTH;
+ private static final int TYPELESS_DATALESS_LENGTH_V3 = BASE_TYPELESS_LENGTH + SENDER_LENGTH + RECIPIENT_LENGTH + ASSET_ID_LENGTH + AMOUNT_LENGTH
+ + DATA_SIZE_LENGTH + IS_TEXT_LENGTH + IS_ENCRYPTED_LENGTH;
+
+ // Other property lengths
+ private static final int MAX_DATA_SIZE = 4000;
+
+ // Constructors
+ public MessageTransaction(PublicKeyAccount sender, String recipient, Long assetId, BigDecimal amount, BigDecimal fee, byte[] data, boolean isText,
+ boolean isEncrypted, long timestamp, byte[] reference, byte[] signature) {
+ super(TransactionType.MESSAGE, fee, sender, timestamp, reference, signature);
+
+ this.version = Transaction.getVersionByTimestamp(this.timestamp);
+ this.sender = sender;
+ this.recipient = new Account(recipient);
+
+ if (assetId != null)
+ this.assetId = assetId;
+ else
+ this.assetId = Asset.QORA;
+
+ this.amount = amount;
+ this.data = data;
+ this.isText = isText;
+ this.isEncrypted = isEncrypted;
+ }
+
+ // Getters/Setters
+
+ public int getVersion() {
+ return this.version;
+ }
+
+ public Account getSender() {
+ return this.sender;
+ }
+
+ public Account getRecipient() {
+ return this.recipient;
+ }
+
+ public Long getAssetId() {
+ return this.assetId;
+ }
+
+ public BigDecimal getAmount() {
+ return this.amount;
+ }
+
+ public byte[] getData() {
+ return this.data;
+ }
+
+ public boolean isText() {
+ return this.isText;
+ }
+
+ public boolean isEncrypted() {
+ return this.isEncrypted;
+ }
+
+ // More information
+
+ public int getDataLength() {
+ if (this.version == 1)
+ return TYPE_LENGTH + TYPELESS_DATALESS_LENGTH_V1 + this.data.length;
+ else
+ return TYPE_LENGTH + TYPELESS_DATALESS_LENGTH_V3 + this.data.length;
+ }
+
+ // Load/Save
+
+ /**
+ * Load MessageTransaction from DB using signature.
+ *
+ * @param signature
+ * @throws NoDataFoundException
+ * if no matching row found
+ * @throws SQLException
+ */
+ protected MessageTransaction(byte[] signature) throws SQLException {
+ super(TransactionType.MESSAGE, signature);
+
+ ResultSet rs = DB.checkedExecute(
+ "SELECT version, sender, recipient, is_text, is_encrypted, amount, asset_id, data FROM MessageTransactions WHERE signature = ?", signature);
+ if (rs == null)
+ throw new NoDataFoundException();
+
+ this.version = rs.getInt(1);
+ this.sender = new PublicKeyAccount(DB.getResultSetBytes(rs.getBinaryStream(2), CREATOR_LENGTH));
+ this.recipient = new Account(rs.getString(3));
+ this.isText = rs.getBoolean(4);
+ this.isEncrypted = rs.getBoolean(5);
+ this.amount = rs.getBigDecimal(6).setScale(8);
+ this.assetId = rs.getLong(7);
+ this.data = DB.getResultSetBytes(rs.getBinaryStream(8));
+ }
+
+ /**
+ * Load MessageTransaction from DB using signature
+ *
+ * @param signature
+ * @return MessageTransaction, or null if not found
+ * @throws SQLException
+ */
+ public static MessageTransaction fromSignature(byte[] signature) throws SQLException {
+ try {
+ return new MessageTransaction(signature);
+ } catch (NoDataFoundException e) {
+ return null;
+ }
+ }
+
+ @Override
+ public void save(Connection connection) throws SQLException {
+ super.save(connection);
+
+ SaveHelper saveHelper = new SaveHelper(connection, "MessageTransactions");
+ saveHelper.bind("signature", this.signature).bind("version", this.version).bind("sender", this.sender.getPublicKey())
+ .bind("recipient", this.recipient.getAddress()).bind("is_text", this.isText).bind("is_encrypted", this.isEncrypted).bind("amount", this.amount)
+ .bind("asset_id", this.assetId).bind("data", this.data);
+ saveHelper.execute();
+ }
+
+ // Converters
+
+ protected static Transaction parse(ByteBuffer byteBuffer) throws ParseException {
+ if (byteBuffer.remaining() < TIMESTAMP_LENGTH)
+ throw new ParseException("Byte data too short for MessageTransaction");
+
+ long timestamp = byteBuffer.getLong();
+ int version = Transaction.getVersionByTimestamp(timestamp);
+
+ int minimumRemaining = version == 1 ? TYPELESS_DATALESS_LENGTH_V1 : TYPELESS_DATALESS_LENGTH_V3;
+ minimumRemaining -= TIMESTAMP_LENGTH; // Already read above
+
+ if (byteBuffer.remaining() < minimumRemaining)
+ throw new ParseException("Byte data too short for MessageTransaction");
+
+ byte[] reference = new byte[REFERENCE_LENGTH];
+ byteBuffer.get(reference);
+ PublicKeyAccount sender = Serialization.deserializePublicKey(byteBuffer);
+ String recipient = Serialization.deserializeRecipient(byteBuffer);
+
+ long assetId;
+ if (version == 1)
+ assetId = Asset.QORA;
+ else
+ assetId = byteBuffer.getLong();
+
+ BigDecimal amount = Serialization.deserializeBigDecimal(byteBuffer);
+
+ int dataSize = byteBuffer.getInt(0);
+ // Don't allow invalid dataSize here to avoid run-time issues
+ if (dataSize > MAX_DATA_SIZE)
+ throw new ParseException("MessageTransaction data size too large");
+
+ byte[] data = new byte[dataSize];
+ byteBuffer.get(data);
+
+ boolean isEncrypted = byteBuffer.get() != 0;
+ boolean isText = byteBuffer.get() != 0;
+
+ BigDecimal fee = Serialization.deserializeBigDecimal(byteBuffer);
+ byte[] signature = new byte[SIGNATURE_LENGTH];
+ byteBuffer.get(signature);
+
+ return new MessageTransaction(sender, recipient, assetId, amount, fee, data, isText, isEncrypted, timestamp, reference, signature);
+ }
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public JSONObject toJSON() throws SQLException {
+ JSONObject json = getBaseJSON();
+
+ json.put("version", this.version);
+ json.put("sender", this.sender.getAddress());
+ json.put("senderPublicKey", HashCode.fromBytes(this.sender.getPublicKey()).toString());
+ json.put("recipient", this.recipient.getAddress());
+ json.put("amount", this.amount.toPlainString());
+ json.put("assetId", this.assetId);
+ json.put("isText", this.isText);
+ json.put("isEncrypted", this.isEncrypted);
+
+ // We can only show plain text as unencoded
+ if (this.isText && !this.isEncrypted)
+ json.put("data", new String(this.data, Charset.forName("UTF-8")));
+ else
+ json.put("data", HashCode.fromBytes(this.data).toString());
+
+ return json;
+ }
+
+ public byte[] toBytes() {
+ try {
+ ByteArrayOutputStream bytes = new ByteArrayOutputStream(getDataLength());
+ bytes.write(Ints.toByteArray(this.type.value));
+ bytes.write(Longs.toByteArray(this.timestamp));
+ bytes.write(this.reference);
+ bytes.write(this.sender.getPublicKey());
+ bytes.write(Base58.decode(this.recipient.getAddress()));
+
+ if (this.version != 1)
+ bytes.write(Longs.toByteArray(this.assetId));
+
+ bytes.write(Serialization.serializeBigDecimal(this.amount));
+
+ bytes.write(Ints.toByteArray(this.data.length));
+ bytes.write(this.data);
+
+ bytes.write((byte) (this.isEncrypted ? 1 : 0));
+ bytes.write((byte) (this.isText ? 1 : 0));
+
+ bytes.write(Serialization.serializeBigDecimal(this.fee));
+ bytes.write(this.signature);
+ return bytes.toByteArray();
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ // Processing
+
+ public ValidationResult isValid(Connection connection) throws SQLException {
+ // Lowest cost checks first
+
+ // Are message transactions even allowed at this point?
+ if (this.version != Transaction.getVersionByTimestamp(this.timestamp))
+ return ValidationResult.NOT_YET_RELEASED;
+
+ if (BlockChain.getHeight() < Block.MESSAGE_RELEASE_HEIGHT)
+ return ValidationResult.NOT_YET_RELEASED;
+
+ // Check data length
+ if (this.data.length < 1 || this.data.length > MAX_DATA_SIZE)
+ return ValidationResult.INVALID_DATA_LENGTH;
+
+ // Check recipient is a valid address
+ if (!Crypto.isValidAddress(this.recipient.getAddress()))
+ return ValidationResult.INVALID_ADDRESS;
+
+ if (this.version == 1) {
+ // Check amount is positive (V1)
+ if (this.amount.compareTo(BigDecimal.ZERO) <= 0)
+ return ValidationResult.NEGATIVE_AMOUNT;
+ } else {
+ // Check amount is not negative (V3) as sending messages without a payment is OK
+ if (this.amount.compareTo(BigDecimal.ZERO) < 0)
+ return ValidationResult.NEGATIVE_AMOUNT;
+ }
+
+ // Check fee is positive
+ if (this.fee.compareTo(BigDecimal.ZERO) <= 0)
+ return ValidationResult.NEGATIVE_FEE;
+
+ // Check reference is correct
+ if (!Arrays.equals(this.sender.getLastReference(), this.reference))
+ return ValidationResult.INVALID_REFERENCE;
+
+ // Does asset exist? (This test not present in gen1)
+ if (this.assetId != Asset.QORA && !Asset.exists(this.assetId))
+ return ValidationResult.ASSET_DOES_NOT_EXIST;
+
+ // If asset is QORA then we need to check amount + fee in one go
+ if (this.assetId == Asset.QORA) {
+ // Check sender has enough funds for amount + fee in QORA
+ if (this.sender.getBalance(Asset.QORA, 1).compareTo(this.amount.add(this.fee)) == -1)
+ return ValidationResult.NO_BALANCE;
+ } else {
+ // Check sender has enough funds for amount in whatever asset
+ if (this.sender.getBalance(this.assetId, 1).compareTo(this.amount) == -1)
+ return ValidationResult.NO_BALANCE;
+
+ // Check sender has enough funds for fee in QORA
+ if (this.sender.getBalance(Asset.QORA, 1).compareTo(this.fee) == -1)
+ return ValidationResult.NO_BALANCE;
+ }
+
+ return ValidationResult.OK;
+ }
+
+ public void process(Connection connection) throws SQLException {
+ this.save(connection);
+
+ // Update sender's balance due to amount
+ this.sender.setConfirmedBalance(connection, this.assetId, this.sender.getConfirmedBalance(this.assetId).subtract(this.amount));
+ // Update sender's balance due to fee
+ this.sender.setConfirmedBalance(connection, Asset.QORA, this.sender.getConfirmedBalance(Asset.QORA).subtract(this.fee));
+
+ // Update recipient's balance
+ this.recipient.setConfirmedBalance(connection, this.assetId, this.recipient.getConfirmedBalance(this.assetId).add(this.amount));
+
+ // Update sender's reference
+ this.sender.setLastReference(connection, this.signature);
+
+ // For QORA amounts only: if recipient has no reference yet, then this is their starting reference
+ if (this.assetId == Asset.QORA && this.recipient.getLastReference() == null)
+ this.recipient.setLastReference(connection, this.signature);
+ }
+
+ public void orphan(Connection connection) throws SQLException {
+ this.delete(connection);
+
+ // Update sender's balance due to amount
+ this.sender.setConfirmedBalance(connection, this.assetId, this.sender.getConfirmedBalance(this.assetId).add(this.amount));
+ // Update sender's balance due to fee
+ this.sender.setConfirmedBalance(connection, Asset.QORA, this.sender.getConfirmedBalance(Asset.QORA).add(this.fee));
+
+ // Update recipient's balance
+ this.recipient.setConfirmedBalance(connection, this.assetId, this.recipient.getConfirmedBalance(this.assetId).subtract(this.amount));
+
+ // Update sender's reference
+ this.sender.setLastReference(connection, this.reference);
+
+ /*
+ * For QORA amounts only: If recipient's last reference is this transaction's signature, then they can't have made any transactions of their own (which
+ * would have changed their last reference) thus this is their first reference so remove it.
+ */
+ if (this.assetId == Asset.QORA && Arrays.equals(this.recipient.getLastReference(), this.signature))
+ this.recipient.setLastReference(connection, null);
+ }
+
+}
diff --git a/src/qora/transaction/Transaction.java b/src/qora/transaction/Transaction.java
index 95e574ee..8bcc58d7 100644
--- a/src/qora/transaction/Transaction.java
+++ b/src/qora/transaction/Transaction.java
@@ -49,7 +49,8 @@ public abstract class Transaction {
// Validation results
public enum ValidationResult {
- OK(1), INVALID_ADDRESS(2), NEGATIVE_AMOUNT(3), NEGATIVE_FEE(4), NO_BALANCE(5), INVALID_REFERENCE(6);
+ OK(1), INVALID_ADDRESS(2), NEGATIVE_AMOUNT(3), NEGATIVE_FEE(4), NO_BALANCE(5), INVALID_REFERENCE(6), INVALID_DATA_LENGTH(27), ASSET_DOES_NOT_EXIST(
+ 29), NOT_YET_RELEASED(1000);
public final int value;
@@ -84,7 +85,7 @@ public abstract class Transaction {
// 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;
+ public static final int REFERENCE_LENGTH = 64;
protected static final int FEE_LENGTH = 8;
public static final int SIGNATURE_LENGTH = 64;
protected static final int BASE_TYPELESS_LENGTH = TIMESTAMP_LENGTH + REFERENCE_LENGTH + FEE_LENGTH + SIGNATURE_LENGTH;
@@ -141,6 +142,13 @@ public abstract class Transaction {
return this.timestamp + (24 * 60 * 60 * 1000);
}
+ /**
+ * Return length of byte[] if {@link Transactions#toBytes()} is called.
+ *
+ * Used to allocate byte[]s or during serialization.
+ *
+ * @return length of serialized transaction
+ */
public abstract int getDataLength();
public boolean hasMinimumFee() {
@@ -170,6 +178,14 @@ public abstract class Transaction {
return recommendedFee.setScale(8);
}
+ public static int getVersionByTimestamp(long timestamp) {
+ if (timestamp < Block.POWFIX_RELEASE_TIMESTAMP) {
+ return 1;
+ } else {
+ return 3;
+ }
+ }
+
/**
* Get block height for this transaction in the blockchain.
*
@@ -240,6 +256,8 @@ public abstract class Transaction {
}
protected void delete(Connection connection) throws SQLException {
+ // 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.
DB.checkedExecute("DELETE FROM Transactions WHERE signature = ?", this.signature);
}
@@ -290,6 +308,13 @@ public abstract class Transaction {
// Converters
+ /**
+ * Deserialize a byte[] into corresponding Transaction subclass.
+ *
+ * @param data
+ * @return subclass of Transaction, e.g. PaymentTransaction
+ * @throws ParseException
+ */
public static Transaction parse(byte[] data) throws ParseException {
if (data == null)
return null;
@@ -310,6 +335,9 @@ public abstract class Transaction {
case PAYMENT:
return PaymentTransaction.parse(byteBuffer);
+ case MESSAGE:
+ return MessageTransaction.parse(byteBuffer);
+
default:
return null;
}
@@ -349,6 +377,8 @@ public abstract class Transaction {
/**
* Serialize transaction as byte[], stripping off trailing signature.
+ *
+ * Used by signature-related methods such as {@link Transaction#calcSignature(PrivateKeyAccount)} and {@link Transaction#isSignatureValid()}
*
* @return byte[]
*/
@@ -370,10 +400,43 @@ public abstract class Transaction {
return this.creator.verify(this.signature, this.toBytesLessSignature());
}
+ /**
+ * Returns whether transaction can be added to the blockchain.
+ *
+ * Checks if transaction can have {@link Transaction#process(Connection)} called.
+ *
+ * Expected to be called within an ongoing SQL Transaction, typically by {@link Block#process(Connection)}, hence the need for the Connection parameter.
+ *
+ * Transactions that have already been processed will return false.
+ *
+ * @param connection
+ * @return true if transaction can be processed, false otherwise
+ * @throws SQLException
+ */
public abstract ValidationResult isValid(Connection connection) throws SQLException;
+ /**
+ * Actually process a transaction, updating the blockchain.
+ *
+ * Processes transaction, updating balances, references, assets, etc. as appropriate.
+ *
+ * Expected to be called within an ongoing SQL Transaction, typically by {@link Block#process(Connection)}, hence the need for the Connection parameter.
+ *
+ * @param connection
+ * @throws SQLException
+ */
public abstract void process(Connection connection) throws SQLException;
+ /**
+ * Undo transaction, updating the blockchain.
+ *
+ * Undoes transaction, updating balances, references, assets, etc. as appropriate.
+ *
+ * Expected to be called within an ongoing SQL Transaction, typically by {@link Block#process(Connection)}, hence the need for the Connection parameter.
+ *
+ * @param connection
+ * @throws SQLException
+ */
public abstract void orphan(Connection connection) throws SQLException;
}
diff --git a/src/test/blocks.java b/src/test/blocks.java
index 44aee7c7..5b658143 100644
--- a/src/test/blocks.java
+++ b/src/test/blocks.java
@@ -67,7 +67,6 @@ public class blocks extends common {
assertFalse(transaction.getFee().compareTo(BigDecimal.ZERO) == 0);
assertNotNull(transaction.getReference());
assertTrue(transaction.isSignatureValid());
- assertEquals(Transaction.ValidationResult.OK, transaction.isValid(connection));
}
// Attempt to load first transaction directly from database
@@ -77,7 +76,21 @@ public class blocks extends common {
assertFalse(transaction.getFee().compareTo(BigDecimal.ZERO) == 0);
assertNotNull(transaction.getReference());
assertTrue(transaction.isSignatureValid());
- assertEquals(Transaction.ValidationResult.OK, transaction.isValid(connection));
+ }
+ }
+
+ @Test
+ public void testBlockSerialization() throws SQLException {
+ try (final Connection connection = DB.getConnection()) {
+ // Block 949 has lots of varied transactions
+ // Blocks 390 & 754 have only payment transactions
+ Block block = Block.fromHeight(754);
+ assertNotNull("Block 754 is required for this test", block);
+ assertTrue(block.isSignatureValid());
+
+ byte[] bytes = block.toBytes();
+
+ assertEquals(block.getDataLength(), bytes.length);
}
}
diff --git a/src/test/migrate.java b/src/test/migrate.java
index 4c89b341..2f4bbf11 100644
--- a/src/test/migrate.java
+++ b/src/test/migrate.java
@@ -31,6 +31,7 @@ import com.google.common.io.CharStreams;
import database.DB;
import qora.block.BlockChain;
+import qora.transaction.Transaction;
import utils.Base58;
public class migrate extends common {
@@ -141,7 +142,7 @@ public class migrate extends common {
PreparedStatement deployATPStmt = c.prepareStatement("INSERT INTO DeployATTransactions "
+ formatWithPlaceholders("signature", "creator", "AT_name", "description", "AT_type", "AT_tags", "creation_bytes", "amount"));
PreparedStatement messagePStmt = c.prepareStatement("INSERT INTO MessageTransactions "
- + formatWithPlaceholders("signature", "sender", "recipient", "is_text", "is_encrypted", "amount", "asset_id", "data"));
+ + formatWithPlaceholders("signature", "version", "sender", "recipient", "is_text", "is_encrypted", "amount", "asset_id", "data"));
PreparedStatement sharedPaymentPStmt = c
.prepareStatement("INSERT INTO SharedTransactionPayments " + formatWithPlaceholders("signature", "recipient", "amount", "asset_id"));
@@ -265,7 +266,8 @@ public class migrate extends common {
fail();
}
- txPStmt.setTimestamp(5, new Timestamp((Long) transaction.get("timestamp")));
+ long transactionTimestamp = ((Long) transaction.get("timestamp")).longValue();
+ txPStmt.setTimestamp(5, new Timestamp(transactionTimestamp));
txPStmt.setBigDecimal(6, BigDecimal.valueOf(Double.valueOf((String) transaction.get("fee")).doubleValue()));
if (milestone_block != null)
@@ -558,18 +560,19 @@ public class migrate extends common {
}
messagePStmt.setBinaryStream(1, new ByteArrayInputStream(txSignature));
- 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);
- messagePStmt.setBigDecimal(6, BigDecimal.valueOf(Double.valueOf((String) transaction.get("amount")).doubleValue()));
+ messagePStmt.setInt(2, Transaction.getVersionByTimestamp(transactionTimestamp));
+ messagePStmt.setBinaryStream(3, new ByteArrayInputStream(addressToPublicKey((String) transaction.get("creator"))));
+ messagePStmt.setString(4, (String) transaction.get("recipient"));
+ messagePStmt.setBoolean(5, isText);
+ messagePStmt.setBoolean(6, isEncrypted);
+ messagePStmt.setBigDecimal(7, BigDecimal.valueOf(Double.valueOf((String) transaction.get("amount")).doubleValue()));
if (transaction.containsKey("asset"))
- messagePStmt.setLong(7, ((Long) transaction.get("asset")).longValue());
+ messagePStmt.setLong(8, ((Long) transaction.get("asset")).longValue());
else
- messagePStmt.setLong(7, 0L); // QORA simulated asset
+ messagePStmt.setLong(8, 0L); // QORA simulated asset
- messagePStmt.setBinaryStream(8, messageDataStream);
+ messagePStmt.setBinaryStream(9, messageDataStream);
messagePStmt.execute();
messagePStmt.clearParameters();
diff --git a/src/test/signatures.java b/src/test/signatures.java
index f3413b08..42437795 100644
--- a/src/test/signatures.java
+++ b/src/test/signatures.java
@@ -2,12 +2,16 @@ package test;
import static org.junit.Assert.*;
+import java.math.BigDecimal;
import java.sql.SQLException;
import org.junit.Test;
+import qora.account.PrivateKeyAccount;
+import qora.block.Block;
import qora.block.GenesisBlock;
import utils.Base58;
+import utils.NTP;
public class signatures extends common {
@@ -22,4 +26,24 @@ public class signatures extends common {
assertEquals(expected58, Base58.encode(block.getSignature()));
}
+ @Test
+ public void testBlockSignature() throws SQLException {
+ int version = 3;
+ byte[] reference = Base58.decode(
+ "BSfgEr6r1rXGGJCv8criR5NcBWfpHdJnm9x5unPwxvojEKCESv1wH1zJm7yvCeC48wshymYtARbHdUojbqWCCWW7h2UTc8g5oEx59C9M41dM7H48My8gVkcEZdxR1of3VgpE5UcowFp3kFC12hVcD9hUttJ2i2nZWMwprbFtUGyVv1U");
+ long timestamp = NTP.getTime() - 5000;
+ BigDecimal generatingBalance = BigDecimal.valueOf(10_000_000L).setScale(8);
+ PrivateKeyAccount generator = new PrivateKeyAccount(
+ new byte[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31 });
+ byte[] atBytes = null;
+ BigDecimal atFees = null;
+
+ Block block = new Block(version, reference, timestamp, generatingBalance, generator, atBytes, atFees);
+
+ block.calcGeneratorSignature();
+ block.calcTransactionsSignature();
+
+ assertTrue(block.isSignatureValid());
+ }
+
}
diff --git a/src/test/transactions.java b/src/test/transactions.java
index 9b726ff7..ec08209a 100644
--- a/src/test/transactions.java
+++ b/src/test/transactions.java
@@ -44,6 +44,8 @@ public class transactions extends common {
Transaction parsedTransaction = Transaction.parse(bytes);
assertTrue(Arrays.equals(transaction.getSignature(), parsedTransaction.getSignature()));
+
+ assertEquals(transaction.getDataLength(), bytes.length);
}
@Test
@@ -63,4 +65,10 @@ public class transactions extends common {
}
}
+ @Test
+ public void testMessageSerialization() throws SQLException, ParseException {
+ // Message transactions went live block 99000
+ // Some transactions to be found in block 99001/2/5/6
+ }
+
}
\ No newline at end of file
diff --git a/src/utils/NTP.java b/src/utils/NTP.java
new file mode 100644
index 00000000..c5a303d9
--- /dev/null
+++ b/src/utils/NTP.java
@@ -0,0 +1,60 @@
+package utils;
+
+import java.net.InetAddress;
+
+import org.apache.commons.net.ntp.NTPUDPClient;
+import org.apache.commons.net.ntp.TimeInfo;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
+public final class NTP {
+
+ private static final Logger LOGGER = LogManager.getLogger(NTP.class);
+ private static final long TIME_TILL_UPDATE = 1000 * 60 * 10;
+ private static final String NTP_SERVER = "pool.ntp.org";
+
+ private static long lastUpdate = 0;
+ private static long offset = 0;
+
+ public static long getTime() {
+ // Every so often use NTP to find out offset between this system's time and internet time
+ if (System.currentTimeMillis() > lastUpdate + TIME_TILL_UPDATE) {
+ updateOffset();
+ lastUpdate = System.currentTimeMillis();
+
+ // Log new value of offset
+ // TODO: LOGGER.info(Lang.getInstance().translate("Adjusting time with %offset% milliseconds.").replace("%offset%", String.valueOf(offset)));
+ LOGGER.info("Adjusting time with %offset% milliseconds.".replace("%offset%", String.valueOf(offset)));
+ }
+
+ // Return time that is nearer internet time
+ return System.currentTimeMillis() + offset;
+ }
+
+ private static void updateOffset() {
+ // Create NTP client
+ NTPUDPClient client = new NTPUDPClient();
+
+ // Set communications timeout
+ client.setDefaultTimeout(10000);
+ try {
+ // Open client (create socket, etc.)
+ client.open();
+
+ // Get time info from NTP server
+ InetAddress hostAddr = InetAddress.getByName(NTP_SERVER);
+ TimeInfo info = client.getTime(hostAddr);
+ info.computeDetails();
+
+ // Cache offset between this system's time and internet time
+ if (info.getOffset() != null)
+ offset = info.getOffset();
+ } catch (Exception e) {
+ // Error while communicating with NTP server - ignored
+ }
+
+ // We're done with NTP client
+ client.close();
+ }
+
+}
+ * {@code int maxMileage = 100_000;}
+ * {@code boolean isAvailable = DB.exists("Cars", "manufacturer = ? AND mileage <= ?", manufacturer, maxMileage);}
+ *
+ * @param tableName
+ * @param whereClause
+ * @param objects
+ * @return true if matching row found in database, false otherwise
+ * @throws SQLException
+ */
+ public static boolean exists(String tableName, String whereClause, Object... objects) throws SQLException {
+ try (final Connection connection = DB.getConnection()) {
+ PreparedStatement preparedStatement = connection
+ .prepareStatement("SELECT TRUE FROM " + tableName + " WHERE " + whereClause + " ORDER BY NULL LIMIT 1");
+ ResultSet resultSet = DB.checkedExecute(preparedStatement);
+ if (resultSet == null)
+ return false;
+
+ return true;
+ }
+ }
+
}
diff --git a/src/database/DatabaseUpdates.java b/src/database/DatabaseUpdates.java
index ca726ce9..de69be03 100644
--- a/src/database/DatabaseUpdates.java
+++ b/src/database/DatabaseUpdates.java
@@ -264,15 +264,16 @@ public class DatabaseUpdates {
case 20:
// Message Transactions
- stmt.execute("CREATE TABLE MessageTransactions (signature Signature, sender QoraPublicKey NOT NULL, recipient QoraAddress NOT NULL, "
- + "is_text BOOLEAN NOT NULL, is_encrypted BOOLEAN NOT NULL, amount QoraAmount NOT NULL, asset_id AssetID NOT NULL, data VARBINARY(4000) NOT NULL, "
- + "PRIMARY KEY (signature), FOREIGN KEY (signature) REFERENCES Transactions (signature) ON DELETE CASCADE)");
+ stmt.execute(
+ "CREATE TABLE MessageTransactions (signature Signature, version TINYINT NOT NULL, sender QoraPublicKey NOT NULL, recipient QoraAddress NOT NULL, "
+ + "is_text BOOLEAN NOT NULL, is_encrypted BOOLEAN NOT NULL, amount QoraAmount NOT NULL, asset_id AssetID NOT NULL, data VARBINARY(4000) NOT NULL, "
+ + "PRIMARY KEY (signature), FOREIGN KEY (signature) REFERENCES Transactions (signature) ON DELETE CASCADE)");
break;
case 21:
// Assets (including QORA coin itself)
stmt.execute(
- "CREATE TABLE Assets (asset_id AssetID IDENTITY, owner QoraAddress NOT NULL, asset_name AssetName NOT NULL, description VARCHAR(4000) NOT NULL, "
+ "CREATE TABLE Assets (asset_id AssetID IDENTITY, owner QoraPublicKey NOT NULL, asset_name AssetName NOT NULL, description VARCHAR(4000) NOT NULL, "
+ "quantity BIGINT NOT NULL, is_divisible BOOLEAN NOT NULL, reference Signature NOT NULL)");
break;
diff --git a/src/qora/account/Account.java b/src/qora/account/Account.java
index 521042aa..b4e59387 100644
--- a/src/qora/account/Account.java
+++ b/src/qora/account/Account.java
@@ -22,7 +22,7 @@ public class Account {
}
public String getAddress() {
- return address;
+ return this.address;
}
@Override
diff --git a/src/qora/account/PrivateKeyAccount.java b/src/qora/account/PrivateKeyAccount.java
index 7cbc3c6e..356a2b02 100644
--- a/src/qora/account/PrivateKeyAccount.java
+++ b/src/qora/account/PrivateKeyAccount.java
@@ -9,6 +9,12 @@ public class PrivateKeyAccount extends PublicKeyAccount {
private byte[] seed;
private Pair
+ * Within this interval, the generating balance stays the same so the current block's generating balance will be returned.
+ *
+ * @return next block's generating balance
+ * @throws SQLException
+ */
+ public BigDecimal getNextBlockGeneratingBalance() throws SQLException {
+ // This block not at the start of an interval?
+ if (this.height % BLOCK_RETARGET_INTERVAL != 0)
+ return this.generatingBalance;
+
+ // Return cached calculation if we have one
+ if (this.cachedNextGeneratingBalance != null)
+ return this.cachedNextGeneratingBalance;
+
+ // Perform calculation
+
+ // 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();
+
+ // 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();
+
+ // 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;
+
+ // 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));
+
+ return this.cachedNextGeneratingBalance;
+ }
+
+ /**
+ * Return expected forging delay, in seconds, since previous block based on block's generating balance.
+ */
+ public static long calcForgingDelay(BigDecimal generatingBalance) {
+ generatingBalance = BlockChain.minMaxBalance(generatingBalance);
+
+ double percentageOfTotal = generatingBalance.divide(BlockChain.MAX_BALANCE).doubleValue();
+ long actualBlockTime = (long) (BlockChain.MIN_BLOCK_TIME + ((BlockChain.MAX_BLOCK_TIME - BlockChain.MIN_BLOCK_TIME) * (1 - percentageOfTotal)));
+
+ return actualBlockTime;
+ }
+
/**
* Return block's transactions.
*
+ * Also checks block's transactions using an HSQLDB "SAVEPOINT" and hence needs to be called within an ongoing SQL Transaction.
+ *
+ * @param connection
+ * @return true if block is valid, false otherwise.
+ * @throws SQLException
+ */
public boolean isValid(Connection connection) throws SQLException {
// TODO
- return false;
+
+ // Check parent blocks exists
+ if (this.reference == null)
+ return false;
+
+ Block parentBlock = this.getParent();
+ if (parentBlock == null)
+ return false;
+
+ // 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())
+ return false;
+
+ // Legacy gen1 test: check timestamp ms is the same as parent timestamp ms?
+ if (this.timestamp % 1000 != parentBlock.getTimestamp() % 1000)
+ return false;
+
+ // Check block version
+ if (this.version != parentBlock.getNextBlockVersion())
+ return false;
+ if (this.version < 2 && (this.atBytes != null || this.atBytes.length > 0 || this.atFees != null || this.atFees.compareTo(BigDecimal.ZERO) > 0))
+ return false;
+
+ // Check generating balance
+ if (this.generatingBalance != 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) {
+ // TODO
+ // try {
+ // AT_Block atBlock = AT_Controller.validateATs(this.getBlockATs(), BlockChain.getHeight() + 1);
+ // this.atFees = atBlock.getTotalFees();
+ // } catch (NoSuchAlgorithmException | AT_Exception e) {
+ // return false;
+ // }
+ }
+
+ // Check transactions
+ DB.createSavepoint(connection, "BLOCK_TRANSACTIONS");
+ // XXX: we might need to catch SQLExceptions and not rollback which could cause a new exception?
+ // OR: catch, attempt to rollback and then re-throw caught exception?
+ // OR: don't catch, attempt to rollback, catch exception during rollback then return false?
+ try {
+ 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)
+ return false;
+
+ // Check transaction is even valid
+ // NOTE: in Gen1 there was an extra block height passed to DeployATTransaction.isValid
+ if (transaction.isValid(connection) != Transaction.ValidationResult.OK)
+ return false;
+
+ // Process transaction to make sure other transactions validate properly
+ try {
+ transaction.process(connection);
+ } catch (Exception e) {
+ // LOGGER.error("Exception during transaction processing, tx " + Base58.encode(transaction.getSignature()), e);
+ return false;
+ }
+ }
+ } finally {
+ // Revert back to savepoint
+ DB.rollbackToSavepoint(connection, "BLOCK_TRANSACTIONS");
+ }
+
+ // Block is valid
+ return true;
}
public void process(Connection connection) throws SQLException {
@@ -519,6 +846,7 @@ public class Block {
Block latestBlock = Block.fromHeight(blockchainHeight);
if (latestBlock != null)
this.reference = latestBlock.getSignature();
+
this.height = blockchainHeight + 1;
this.save(connection);
diff --git a/src/qora/block/BlockChain.java b/src/qora/block/BlockChain.java
index 28ed3127..5afe5310 100644
--- a/src/qora/block/BlockChain.java
+++ b/src/qora/block/BlockChain.java
@@ -1,5 +1,6 @@
package qora.block;
+import java.math.BigDecimal;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
@@ -13,6 +14,23 @@ import qora.assets.Asset;
*/
public class BlockChain {
+ /**
+ * Minimum Qora balance.
+ */
+ public static final BigDecimal MIN_BALANCE = BigDecimal.valueOf(1L).setScale(8);
+ /**
+ * Maximum Qora balance.
+ */
+ public static final BigDecimal MAX_BALANCE = BigDecimal.valueOf(10_000_000_000L).setScale(8);
+ /**
+ * Minimum target time between blocks, in seconds.
+ */
+ public static final long MIN_BLOCK_TIME = 60;
+ /**
+ * Maximum target time between blocks, in seconds.
+ */
+ public static final long MAX_BLOCK_TIME = 300;
+
/**
* Some sort start-up/initialization/checking method.
*
@@ -84,4 +102,17 @@ public class BlockChain {
}
}
+ /**
+ * Return Qora balance adjusted to within min/max limits.
+ */
+ public static BigDecimal minMaxBalance(BigDecimal balance) {
+ if (balance.compareTo(MIN_BALANCE) < 0)
+ return MIN_BALANCE;
+
+ if (balance.compareTo(MAX_BALANCE) > 0)
+ return MAX_BALANCE;
+
+ return balance;
+ }
+
}
diff --git a/src/qora/block/GenesisBlock.java b/src/qora/block/GenesisBlock.java
index 7fc8ff8b..e359b900 100644
--- a/src/qora/block/GenesisBlock.java
+++ b/src/qora/block/GenesisBlock.java
@@ -13,7 +13,6 @@ import com.google.common.primitives.Bytes;
import com.google.common.primitives.Longs;
import qora.account.GenesisAccount;
-import qora.account.PrivateKeyAccount;
import qora.crypto.Crypto;
import qora.transaction.GenesisTransaction;
import qora.transaction.Transaction;
@@ -33,10 +32,9 @@ public class GenesisBlock extends Block {
// Constructors
protected GenesisBlock() {
super(GENESIS_BLOCK_VERSION, GENESIS_REFERENCE, GENESIS_TIMESTAMP, GENESIS_GENERATING_BALANCE, GENESIS_GENERATOR, GENESIS_GENERATOR_SIGNATURE,
- GENESIS_TRANSACTIONS_SIGNATURE, null, null);
+ GENESIS_TRANSACTIONS_SIGNATURE, null, null, new ArrayList