atStateData = this.repository.getATRepository().getBlockATStatesAtHeight(this.blockData.getHeight());
// The number of AT states fetched from repository should correspond with Block's atCount
if (atStateData.size() != this.blockData.getATCount())
@@ -531,6 +524,10 @@ public class Block {
if (this.blockData.getGeneratorSignature() == null)
throw new IllegalStateException("Cannot calculate transactions signature as block has no generator signature");
+ // Already added?
+ if (this.transactions.contains(transactionData))
+ return true;
+
// Check there is space in block
try {
if (BlockTransformer.getDataLength(this) + TransactionTransformer.getDataLength(transactionData) > MAX_BLOCK_BYTES)
@@ -542,12 +539,16 @@ public class Block {
// Add to block
this.transactions.add(Transaction.fromData(this.repository, transactionData));
+ // Re-sort
+ this.transactions.sort(Transaction.getComparator());
+
// Update transaction count
this.blockData.setTransactionCount(this.blockData.getTransactionCount() + 1);
// Update totalFees
this.blockData.setTotalFees(this.blockData.getTotalFees().add(transactionData.getFee()));
+ // We've added a transaction, so recalculate transactions signature
calcTransactionsSignature();
return true;
@@ -601,6 +602,14 @@ public class Block {
}
}
+ /**
+ * Recalculate block's generator and transactions signatures, thus giving block full signature.
+ *
+ * Note: Block instance must have been constructed with a PrivateKeyAccount generator or this call will throw an IllegalStateException.
+ *
+ * @throws IllegalStateException
+ * if block's {@code generator} is not a {@code PrivateKeyAccount}.
+ */
public void sign() {
this.calcGeneratorSignature();
this.calcTransactionsSignature();
@@ -608,6 +617,11 @@ public class Block {
this.blockData.setSignature(this.getSignature());
}
+ /**
+ * Returns whether this block's signatures are valid.
+ *
+ * @return true if both generator and transaction signatures are valid, false otherwise
+ */
public boolean isSignatureValid() {
try {
// Check generator's signature first
@@ -658,7 +672,7 @@ public class Block {
if (this.blockData.getTimestamp() - BlockChain.BLOCK_TIMESTAMP_MARGIN > NTP.getTime())
return ValidationResult.TIMESTAMP_IN_FUTURE;
- // Legacy gen1 test: check timestamp ms is the same as parent timestamp ms?
+ // Legacy gen1 test: check timestamp milliseconds is the same as parent timestamp milliseconds?
if (this.blockData.getTimestamp() % 1000 != parentBlockData.getTimestamp() % 1000)
return ValidationResult.TIMESTAMP_MS_INCORRECT;
@@ -685,8 +699,8 @@ public class Block {
if (hashValue.compareTo(target) >= 0)
return ValidationResult.GENERATOR_NOT_ACCEPTED;
- // XXX Odd gen1 test: "CHECK IF FIRST BLOCK OF USER"
- // Is the comment wrong? Does each second elapsed allows generator to test a new "target" window against hashValue?
+ // Odd gen1 comment: "CHECK IF FIRST BLOCK OF USER"
+ // Each second elapsed allows generator to test a new "target" window against hashValue
if (hashValue.compareTo(lowerTarget) < 0)
return ValidationResult.GENERATOR_NOT_ACCEPTED;
@@ -694,8 +708,16 @@ public class Block {
if (this.blockData.getATCount() != 0) {
// Locally generated AT states should be valid so no need to re-execute them
if (this.ourAtStates != this.getATStates()) {
- // Otherwise, check locally generated AT states against ones received from elsewhere?
- this.executeATs();
+ // For old v1 CIYAM ATs we blindly accept them
+ if (this.blockData.getVersion() < 4) {
+ this.ourAtStates = this.atStates;
+ this.ourAtFees = this.blockData.getATFees();
+ } else {
+ // Generate local AT states for comparison
+ this.executeATs();
+ }
+
+ // Check locally generated AT states against ones received from elsewhere
if (this.ourAtStates.size() != this.blockData.getATCount())
return ValidationResult.AT_STATES_MISMATCH;
@@ -772,40 +794,66 @@ public class Block {
* Execute CIYAM ATs for this block.
*
* This needs to be done locally for all blocks, regardless of origin.
- * This method is called by isValid.
+ * Typically called by isValid() or new block constructor.
*
* After calling, AT-generated transactions are prepended to the block's transactions and AT state data is generated.
*
- * This method is not needed if fetching an existing block from the repository.
+ * Updates this.ourAtStates (local version) and this.ourAtFees (remote/imported/loaded version).
*
- * Updates this.ourAtStates and this.ourAtFees.
+ * Note: this method does not store new AT state data into repository - that is handled by process().
+ *
+ * This method is not needed if fetching an existing block from the repository as AT state data will be loaded from repository as well.
*
* @see #isValid()
*
* @throws DataException
*
*/
- public void executeATs() throws DataException {
+ private void executeATs() throws DataException {
// We're expecting a lack of AT state data at this point.
if (this.ourAtStates != null)
throw new IllegalStateException("Attempted to execute ATs when block's local AT state data already exists");
- // For old v1 CIYAM ATs we blindly accept them
- if (this.blockData.getVersion() < 4) {
- this.ourAtStates = this.atStates;
- this.ourAtFees = this.blockData.getATFees();
- return;
- }
+ // AT-Transactions generated by running ATs, to be prepended to block's transactions
+ List allATTransactions = new ArrayList();
+
+ this.ourAtStates = new ArrayList();
+ this.ourAtFees = BigDecimal.ZERO.setScale(8);
// Find all executable ATs, ordered by earliest creation date first
+ List executableATs = this.repository.getATRepository().getAllExecutableATs();
// Run each AT, appends AT-Transactions and corresponding AT states, to our lists
+ for (ATData atData : executableATs) {
+ AT at = new AT(this.repository, atData);
+ List atTransactions = at.run(this.blockData.getTimestamp());
- // Finally prepend our entire AT-Transactions/states to block's transactions/states, adjust fees, etc.
+ allATTransactions.addAll(atTransactions);
- // Note: store locally-calculated AT states separately to this.atStates so we can compare them in isValid()
+ ATStateData atStateData = at.getATStateData();
+ this.ourAtStates.add(atStateData);
+
+ this.ourAtFees = this.ourAtFees.add(atStateData.getFees());
+ }
+
+ // Prepend our entire AT-Transactions/states to block's transactions
+ this.transactions.addAll(0, allATTransactions);
+
+ // Re-sort
+ this.transactions.sort(Transaction.getComparator());
+
+ // Update transaction count
+ this.blockData.setTransactionCount(this.blockData.getTransactionCount() + 1);
+
+ // We've added transactions, so recalculate transactions signature
+ calcTransactionsSignature();
}
+ /**
+ * Process block, and its transactions, adding them to the blockchain.
+ *
+ * @throws DataException
+ */
public void process() throws DataException {
// Process transactions (we'll link them to this block after saving the block itself)
// AT-generated transactions are already added to our transactions so no special handling is needed here.
@@ -846,9 +894,19 @@ public class Block {
BlockTransactionData blockTransactionData = new BlockTransactionData(this.getSignature(), sequence,
transaction.getTransactionData().getSignature());
this.repository.getBlockRepository().save(blockTransactionData);
+
+ // No longer unconfirmed
+ this.repository.getTransactionRepository().confirmTransaction(transaction.getTransactionData().getSignature());
}
}
+ /**
+ * Removes block from blockchain undoing transactions.
+ *
+ * Note: it is up to the caller to re-add any of the block's transactions back to the unconfirmed transactions pile.
+ *
+ * @throws DataException
+ */
public void orphan() throws DataException {
// Orphan transactions in reverse order, and unlink them from this block
// AT-generated transactions are already added to our transactions so no special handling is needed here.
diff --git a/src/qora/block/BlockGenerator.java b/src/qora/block/BlockGenerator.java
new file mode 100644
index 00000000..cdd54d2a
--- /dev/null
+++ b/src/qora/block/BlockGenerator.java
@@ -0,0 +1,121 @@
+package qora.block;
+
+import java.util.Arrays;
+import java.util.List;
+
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
+import data.block.BlockData;
+import data.transaction.TransactionData;
+import qora.account.PrivateKeyAccount;
+import qora.block.Block.ValidationResult;
+import repository.BlockRepository;
+import repository.DataException;
+import repository.Repository;
+import repository.RepositoryManager;
+
+// Forging new blocks
+
+// How is the private key going to be supplied?
+
+public class BlockGenerator extends Thread {
+
+ // Properties
+ private byte[] generatorPrivateKey;
+ private PrivateKeyAccount generator;
+ private Block previousBlock;
+ private Block newBlock;
+ private boolean running;
+
+ // Other properties
+ private static final Logger LOGGER = LogManager.getLogger(BlockGenerator.class);
+
+ // Constructors
+
+ public BlockGenerator(byte[] generatorPrivateKey) {
+ this.generatorPrivateKey = generatorPrivateKey;
+ this.previousBlock = null;
+ this.newBlock = null;
+ this.running = true;
+ }
+
+ // Main thread loop
+ @Override
+ public void run() {
+ try (final Repository repository = RepositoryManager.getRepository()) {
+ generator = new PrivateKeyAccount(repository, generatorPrivateKey);
+
+ // Going to need this a lot...
+ BlockRepository blockRepository = repository.getBlockRepository();
+
+ while (running) {
+ // Check blockchain hasn't changed
+ BlockData lastBlockData = blockRepository.getLastBlock();
+ if (previousBlock == null || !Arrays.equals(previousBlock.getSignature(), lastBlockData.getSignature())) {
+ previousBlock = new Block(repository, lastBlockData);
+ newBlock = null;
+ }
+
+ // Do we need to build a potential new block?
+ if (newBlock == null)
+ newBlock = new Block(repository, previousBlock.getBlockData(), generator);
+
+ // Is new block valid yet? (Before adding unconfirmed transactions)
+ if (newBlock.isValid() == ValidationResult.OK) {
+ // Add unconfirmed transactions
+ addUnconfirmedTransactions(repository, newBlock);
+
+ // Sign to create block's signature
+ newBlock.sign();
+
+ // If newBlock is still valid then we can use it
+ ValidationResult validationResult = newBlock.isValid();
+ if (validationResult == ValidationResult.OK) {
+ // Add to blockchain - something else will notice and broadcast new block to network
+ try {
+ newBlock.process();
+ LOGGER.info("Generated new block: " + newBlock.getBlockData().getHeight());
+ repository.saveChanges();
+ } catch (DataException e) {
+ // Unable to process block - report and discard
+ LOGGER.error("Unable to process newly generated block?", e);
+ newBlock = null;
+ }
+ } else {
+ // No longer valid? Report and discard
+ LOGGER.error("Valid, generated block now invalid '" + validationResult.name() + "' after adding unconfirmed transactions?");
+ newBlock = null;
+ }
+ }
+
+ // Sleep for a while
+ try {
+ Thread.sleep(1000);
+ } catch (InterruptedException e) {
+ // We've been interrupted - time to exit
+ return;
+ }
+ }
+ } catch (DataException e) {
+ LOGGER.warn("Repository issue while running block generator", e);
+ }
+ }
+
+ private void addUnconfirmedTransactions(Repository repository, Block newBlock) throws DataException {
+ // Grab all unconfirmed transactions (already sorted)
+ List unconfirmedTransactions = repository.getTransactionRepository().getAllUnconfirmedTransactions();
+
+ // Attempt to add transactions until block is full, or we run out
+ for (TransactionData transactionData : unconfirmedTransactions)
+ if (!newBlock.addTransaction(transactionData))
+ break;
+ }
+
+ public void shutdown() {
+ this.running = false;
+ // Interrupt too, absorbed by HSQLDB but could be caught by Thread.sleep()
+ Thread.currentThread().interrupt();
+ }
+
+}
diff --git a/src/qora/transaction/ATTransaction.java b/src/qora/transaction/ATTransaction.java
index 790ba578..2a12e2fd 100644
--- a/src/qora/transaction/ATTransaction.java
+++ b/src/qora/transaction/ATTransaction.java
@@ -5,6 +5,8 @@ import java.util.Arrays;
import java.util.Collections;
import java.util.List;
+import com.google.common.primitives.Bytes;
+
import data.assets.AssetData;
import data.transaction.ATTransactionData;
import data.transaction.TransactionData;
@@ -13,6 +15,8 @@ import qora.assets.Asset;
import qora.crypto.Crypto;
import repository.DataException;
import repository.Repository;
+import transform.TransformationException;
+import transform.transaction.ATTransactionTransformer;
public class ATTransaction extends Transaction {
@@ -28,6 +32,18 @@ public class ATTransaction extends Transaction {
super(repository, transactionData);
this.atTransactionData = (ATTransactionData) this.transactionData;
+
+ // Check whether we need to generate the ATTransaction's pseudo-signature
+ if (this.atTransactionData.getSignature() == null) {
+ // Signature is SHA2-256 of serialized transaction data, duplicated to make standard signature size of 64 bytes.
+ try {
+ byte[] digest = Crypto.digest(ATTransactionTransformer.toBytes(transactionData));
+ byte[] signature = Bytes.concat(digest, digest);
+ this.atTransactionData.setSignature(signature);
+ } catch (TransformationException e) {
+ throw new RuntimeException("Couldn't transform AT Transaction into bytes", e);
+ }
+ }
}
// More information
@@ -92,6 +108,14 @@ public class ATTransaction extends Transaction {
return ValidationResult.INVALID_DATA_LENGTH;
BigDecimal amount = this.atTransactionData.getAmount();
+ byte[] message = this.atTransactionData.getMessage();
+
+ // We can only have either message or amount
+ boolean amountIsZero = amount.compareTo(BigDecimal.ZERO.setScale(8)) == 0;
+ boolean messageIsEmpty = message.length == 0;
+
+ if ((messageIsEmpty && amountIsZero) || (!messageIsEmpty && !amountIsZero))
+ return ValidationResult.INVALID_AT_TRANSACTION;
// If we have no payment then we're done
if (amount == null)
diff --git a/src/qora/transaction/ArbitraryTransaction.java b/src/qora/transaction/ArbitraryTransaction.java
index ef82afaa..62e8a633 100644
--- a/src/qora/transaction/ArbitraryTransaction.java
+++ b/src/qora/transaction/ArbitraryTransaction.java
@@ -4,13 +4,18 @@ import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.math.BigDecimal;
+import java.nio.file.DirectoryNotEmptyException;
import java.nio.file.Files;
+import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
import data.PaymentData;
import data.transaction.ArbitraryTransactionData;
import data.transaction.TransactionData;
@@ -31,6 +36,9 @@ public class ArbitraryTransaction extends Transaction {
// Properties
private ArbitraryTransactionData arbitraryTransactionData;
+ // Other properties
+ private static final Logger LOGGER = LogManager.getLogger(ArbitraryTransaction.class);
+
// Other useful constants
public static final int MAX_DATA_SIZE = 4000;
@@ -141,8 +149,11 @@ public class ArbitraryTransaction extends Transaction {
// Now store actual data somewhere, e.g. /arbitrary///-.raw
Account sender = this.getSender();
int blockHeight = this.repository.getBlockRepository().getBlockchainHeight();
- String dataPathname = Settings.getInstance().getUserpath() + "arbitrary" + File.separator + sender.getAddress() + File.separator + blockHeight
- + File.separator + Base58.encode(arbitraryTransactionData.getSignature()) + "-" + arbitraryTransactionData.getService() + ".raw";
+
+ String senderPathname = Settings.getInstance().getUserpath() + "arbitrary" + File.separator + sender.getAddress();
+ String blockPathname = senderPathname + File.separator + blockHeight;
+ String dataPathname = blockPathname + File.separator + Base58.encode(arbitraryTransactionData.getSignature()) + "-"
+ + arbitraryTransactionData.getService() + ".raw";
Path dataPath = Paths.get(dataPathname);
@@ -150,16 +161,16 @@ public class ArbitraryTransaction extends Transaction {
try {
Files.createDirectories(dataPath.getParent());
} catch (IOException e) {
- // TODO Auto-generated catch block
- e.printStackTrace();
+ LOGGER.error("Unable to create arbitrary transaction directory", e);
+ throw new DataException("Unable to create arbitrary transaction directory", e);
}
// Output actual transaction data
try (OutputStream dataOut = Files.newOutputStream(dataPath)) {
dataOut.write(rawData);
} catch (IOException e) {
- // TODO Auto-generated catch block
- e.printStackTrace();
+ LOGGER.error("Unable to store arbitrary transaction data", e);
+ throw new DataException("Unable to store arbitrary transaction data", e);
}
}
@@ -176,15 +187,27 @@ public class ArbitraryTransaction extends Transaction {
// Delete corresponding data file (if any - storing raw data is optional)
Account sender = this.getSender();
int blockHeight = this.repository.getBlockRepository().getBlockchainHeight();
- String dataPathname = Settings.getInstance().getUserpath() + "arbitrary" + File.separator + sender.getAddress() + File.separator + blockHeight
- + File.separator + Base58.encode(arbitraryTransactionData.getSignature()) + "-" + arbitraryTransactionData.getService() + ".raw";
- Path dataPath = Paths.get(dataPathname);
+ String senderPathname = Settings.getInstance().getUserpath() + "arbitrary" + File.separator + sender.getAddress();
+ String blockPathname = senderPathname + File.separator + blockHeight;
+ String dataPathname = blockPathname + File.separator + Base58.encode(arbitraryTransactionData.getSignature()) + "-"
+ + arbitraryTransactionData.getService() + ".raw";
+
try {
- Files.deleteIfExists(dataPath);
+ // Delete the actual arbitrary data
+ Files.delete(Paths.get(dataPathname));
+
+ // If block-directory now empty, delete that too
+ Files.delete(Paths.get(blockPathname));
+
+ // If sender-directory now empty, delete that too
+ Files.delete(Paths.get(senderPathname));
+ } catch (NoSuchFileException e) {
+ LOGGER.warn("Unable to remove old arbitrary transaction data at " + dataPathname);
+ } catch (DirectoryNotEmptyException e) {
+ // This happens when block-directory or sender-directory is not empty but is OK
} catch (IOException e) {
- // TODO Auto-generated catch block
- e.printStackTrace();
+ LOGGER.warn("IOException when trying to remove old arbitrary transaction data", e);
}
// Delete this transaction itself
diff --git a/src/qora/transaction/CreatePollTransaction.java b/src/qora/transaction/CreatePollTransaction.java
index 6658c3ba..48ae3de1 100644
--- a/src/qora/transaction/CreatePollTransaction.java
+++ b/src/qora/transaction/CreatePollTransaction.java
@@ -80,7 +80,7 @@ public class CreatePollTransaction extends Transaction {
@Override
public ValidationResult isValid() throws DataException {
// Are CreatePollTransactions even allowed at this point?
- // XXX In gen1 this used NTP.getTime() but surely the transaction's timestamp should be used?
+ // In gen1 this used NTP.getTime() but surely the transaction's timestamp should be used
if (this.createPollTransactionData.getTimestamp() < BlockChain.getVotingReleaseTimestamp())
return ValidationResult.NOT_YET_RELEASED;
@@ -106,7 +106,7 @@ public class CreatePollTransaction extends Transaction {
if (this.repository.getVotingRepository().pollExists(createPollTransactionData.getPollName()))
return ValidationResult.POLL_ALREADY_EXISTS;
- // XXX In gen1 we tested for votes but how can there be any if poll doesn't exist?
+ // In gen1 we tested for presence of existing votes but how could there be any if poll doesn't exist?
// Check number of options
List pollOptions = createPollTransactionData.getPollOptions();
diff --git a/src/qora/transaction/DeployATTransaction.java b/src/qora/transaction/DeployATTransaction.java
index e80412bf..042f3c24 100644
--- a/src/qora/transaction/DeployATTransaction.java
+++ b/src/qora/transaction/DeployATTransaction.java
@@ -8,6 +8,8 @@ import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
+import org.ciyam.at.MachineState;
+
import com.google.common.base.Utf8;
import data.transaction.DeployATTransactionData;
@@ -173,6 +175,12 @@ public class DeployATTransaction extends Transaction {
// Check creation bytes are valid (for v2+)
if (this.getVersion() >= 2) {
// Do actual validation
+ try {
+ new MachineState(deployATTransactionData.getCreationBytes());
+ } catch (IllegalArgumentException e) {
+ // Not valid
+ return ValidationResult.INVALID_CREATION_BYTES;
+ }
} else {
// Skip validation for old, dead ATs
}
diff --git a/src/qora/transaction/IssueAssetTransaction.java b/src/qora/transaction/IssueAssetTransaction.java
index d3954df6..e1b38e7d 100644
--- a/src/qora/transaction/IssueAssetTransaction.java
+++ b/src/qora/transaction/IssueAssetTransaction.java
@@ -82,7 +82,7 @@ public class IssueAssetTransaction extends Transaction {
@Override
public ValidationResult isValid() throws DataException {
// Are IssueAssetTransactions even allowed at this point?
- // XXX In gen1 this used NTP.getTime() but surely the transaction's timestamp should be used?
+ // In gen1 this used NTP.getTime() but surely the transaction's timestamp should be used
if (this.issueAssetTransactionData.getTimestamp() < BlockChain.getAssetsReleaseTimestamp())
return ValidationResult.NOT_YET_RELEASED;
@@ -119,7 +119,7 @@ public class IssueAssetTransaction extends Transaction {
if (issuer.getConfirmedBalance(Asset.QORA).compareTo(issueAssetTransactionData.getFee()) < 0)
return ValidationResult.NO_BALANCE;
- // XXX: Surely we want to check the asset name isn't already taken? This check is not present in gen1.
+ // Check the asset name isn't already taken. This check is not present in gen1.
if (issueAssetTransactionData.getTimestamp() >= BlockChain.getIssueAssetV2Timestamp())
if (this.repository.getAssetRepository().assetExists(issueAssetTransactionData.getAssetName()))
return ValidationResult.ASSET_ALREADY_EXISTS;
diff --git a/src/qora/transaction/Transaction.java b/src/qora/transaction/Transaction.java
index f532e3bf..d93ad5c4 100644
--- a/src/qora/transaction/Transaction.java
+++ b/src/qora/transaction/Transaction.java
@@ -1,7 +1,9 @@
package qora.transaction;
import java.math.BigDecimal;
+import java.math.BigInteger;
import java.math.MathContext;
+import java.util.Comparator;
import java.util.List;
import java.util.Map;
@@ -95,8 +97,10 @@ public abstract class Transaction {
INVALID_ORDER_CREATOR(33),
INVALID_PAYMENTS_COUNT(34),
NEGATIVE_PRICE(35),
+ INVALID_CREATION_BYTES(36),
INVALID_TAGS_LENGTH(37),
INVALID_AT_TYPE_LENGTH(38),
+ INVALID_AT_TRANSACTION(39),
ASSET_ALREADY_EXISTS(43),
NOT_YET_RELEASED(1000);
@@ -347,6 +351,7 @@ public abstract class Transaction {
* @return BlockData, or null if transaction is not in a Block
* @throws DataException
*/
+ @Deprecated
public BlockData getBlock() throws DataException {
return this.repository.getTransactionRepository().getBlockDataFromSignature(this.transactionData.getSignature());
}
@@ -430,4 +435,51 @@ public abstract class Transaction {
*/
public abstract void orphan() throws DataException;
+ // Comparison
+
+ /** Returns comparator that sorts ATTransactions first, then by timestamp, then by signature */
+ public static Comparator getComparator() {
+ class TransactionComparator implements Comparator {
+
+ // Compare by type, timestamp, then signature
+ @Override
+ public int compare(Transaction t1, Transaction t2) {
+ TransactionData td1 = t1.getTransactionData();
+ TransactionData td2 = t2.getTransactionData();
+
+ // AT transactions come before non-AT transactions
+ if (td1.getType() == TransactionType.AT && td2.getType() != TransactionType.AT)
+ return -1;
+ // Non-AT transactions come after AT transactions
+ if (td1.getType() != TransactionType.AT && td2.getType() == TransactionType.AT)
+ return 1;
+
+ // Both transactions are either AT or non-AT so compare timestamps
+ int result = Long.compare(td1.getTimestamp(), td2.getTimestamp());
+
+ if (result == 0)
+ // Same timestamp so compare signatures
+ result = new BigInteger(td1.getSignature()).compareTo(new BigInteger(td2.getSignature()));
+
+ return result;
+ }
+
+ }
+
+ return new TransactionComparator();
+ }
+
+ @Override
+ public int hashCode() {
+ return this.transactionData.hashCode();
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (!(other instanceof TransactionData))
+ return false;
+
+ return this.transactionData.equals(other);
+ }
+
}
diff --git a/src/qora/transaction/TransferAssetTransaction.java b/src/qora/transaction/TransferAssetTransaction.java
index 5325c9f0..4bf0703f 100644
--- a/src/qora/transaction/TransferAssetTransaction.java
+++ b/src/qora/transaction/TransferAssetTransaction.java
@@ -85,7 +85,7 @@ public class TransferAssetTransaction extends Transaction {
@Override
public ValidationResult isValid() throws DataException {
// Are TransferAssetTransactions even allowed at this point?
- // XXX In gen1 this used NTP.getTime() but surely the transaction's timestamp should be used?
+ // In gen1 this used NTP.getTime() but surely the transaction's timestamp should be used
if (this.transferAssetTransactionData.getTimestamp() < BlockChain.getAssetsReleaseTimestamp())
return ValidationResult.NOT_YET_RELEASED;
diff --git a/src/qora/transaction/VoteOnPollTransaction.java b/src/qora/transaction/VoteOnPollTransaction.java
index 83600fab..6d72ed35 100644
--- a/src/qora/transaction/VoteOnPollTransaction.java
+++ b/src/qora/transaction/VoteOnPollTransaction.java
@@ -72,7 +72,7 @@ public class VoteOnPollTransaction extends Transaction {
@Override
public ValidationResult isValid() throws DataException {
// Are VoteOnPollTransactions even allowed at this point?
- // XXX In gen1 this used NTP.getTime() but surely the transaction's timestamp should be used?
+ // In gen1 this used NTP.getTime() but surely the transaction's timestamp should be used
if (this.voteOnPollTransactionData.getTimestamp() < BlockChain.getVotingReleaseTimestamp())
return ValidationResult.NOT_YET_RELEASED;
diff --git a/src/repository/ATRepository.java b/src/repository/ATRepository.java
index 70e494ef..355baffb 100644
--- a/src/repository/ATRepository.java
+++ b/src/repository/ATRepository.java
@@ -9,18 +9,71 @@ public interface ATRepository {
// CIYAM AutomatedTransactions
+ /** Returns ATData using AT's address or null if none found */
public ATData fromATAddress(String atAddress) throws DataException;
+ /** Returns list of executable ATs, empty if none found */
+ public List getAllExecutableATs() throws DataException;
+
+ /** Returns creation block height given AT's address or null if not found */
+ public Integer getATCreationBlockHeight(String atAddress) throws DataException;
+
+ /** Saves ATData into repository */
public void save(ATData atData) throws DataException;
+ /** Removes an AT from repository, including associated ATStateData */
public void delete(String atAddress) throws DataException;
// AT States
- public ATStateData getATState(String atAddress, int height) throws DataException;
+ /**
+ * Returns ATStateData for an AT at given height.
+ *
+ * @param atAddress
+ * - AT's address
+ * @param height
+ * - block height
+ * @return ATStateData for AT at given height or null if none found
+ */
+ public ATStateData getATStateAtHeight(String atAddress, int height) throws DataException;
- public List getBlockATStatesFromHeight(int height) throws DataException;
+ /**
+ * Returns latest ATStateData for an AT.
+ *
+ * As ATs don't necessarily run every block, this will return the ATStateData with the greatest height.
+ *
+ * @param atAddress
+ * - AT's address
+ * @return ATStateData for AT with greatest height or null if none found
+ */
+ public ATStateData getLatestATState(String atAddress) throws DataException;
+ /**
+ * Returns all ATStateData for a given block height.
+ *
+ * Unlike getATState, only returns ATStateData saved at the given height.
+ *
+ * @param height
+ * - block height
+ * @return list of ATStateData for given height, empty list if none found
+ * @throws DataException
+ */
+ public List getBlockATStatesAtHeight(int height) throws DataException;
+
+ /**
+ * Save ATStateData into repository.
+ *
+ * Note: Requires at least these ATStateData properties to be filled, or an IllegalArgumentException will be thrown:
+ *
+ *
+ * - creation
+ * - stateHash
+ * - height
+ *
+ *
+ * @param atStateData
+ * @throws IllegalArgumentException
+ */
public void save(ATStateData atStateData) throws DataException;
/** Delete AT's state data at this height */
diff --git a/src/repository/BlockRepository.java b/src/repository/BlockRepository.java
index 6077cc3a..829fcf30 100644
--- a/src/repository/BlockRepository.java
+++ b/src/repository/BlockRepository.java
@@ -8,10 +8,31 @@ import data.transaction.TransactionData;
public interface BlockRepository {
+ /**
+ * Returns BlockData from repository using block signature.
+ *
+ * @param signature
+ * @return block data, or null if not found in blockchain.
+ * @throws DataException
+ */
public BlockData fromSignature(byte[] signature) throws DataException;
+ /**
+ * Returns BlockData from repository using block reference.
+ *
+ * @param reference
+ * @return block data, or null if not found in blockchain.
+ * @throws DataException
+ */
public BlockData fromReference(byte[] reference) throws DataException;
+ /**
+ * Returns BlockData from repository using block height.
+ *
+ * @param height
+ * @return block data, or null if not found in blockchain.
+ * @throws DataException
+ */
public BlockData fromHeight(int height) throws DataException;
/**
@@ -38,14 +59,58 @@ public interface BlockRepository {
*/
public BlockData getLastBlock() throws DataException;
+ /**
+ * Returns block's transactions given block's signature.
+ *
+ * This is typically used by Block.getTransactions() which uses lazy-loading of transactions.
+ *
+ * @param signature
+ * @return list of transactions, or null if block not found in blockchain.
+ * @throws DataException
+ */
public List getTransactionsFromSignature(byte[] signature) throws DataException;
+ /**
+ * Saves block into repository.
+ *
+ * @param blockData
+ * @throws DataException
+ */
public void save(BlockData blockData) throws DataException;
+ /**
+ * Deletes block from repository.
+ *
+ * @param blockData
+ * @throws DataException
+ */
public void delete(BlockData blockData) throws DataException;
+ /**
+ * Saves a block-transaction mapping into the repository.
+ *
+ * This essentially links a transaction to a specific block.
+ * Transactions cannot be mapped to more than one block, so attempts will result in a DataException.
+ *
+ * Note: it is the responsibility of the caller to maintain contiguous "sequence" values
+ * for all transactions mapped to a block.
+ *
+ * @param blockTransactionData
+ * @throws DataException
+ */
public void save(BlockTransactionData blockTransactionData) throws DataException;
+ /**
+ * Deletes a block-transaction mapping from the repository.
+ *
+ * This essentially unlinks a transaction from a specific block.
+ *
+ * Note: it is the responsibility of the caller to maintain contiguous "sequence" values
+ * for all transactions mapped to a block.
+ *
+ * @param blockTransactionData
+ * @throws DataException
+ */
public void delete(BlockTransactionData blockTransactionData) throws DataException;
}
diff --git a/src/repository/TransactionRepository.java b/src/repository/TransactionRepository.java
index b15a36b3..7afce46c 100644
--- a/src/repository/TransactionRepository.java
+++ b/src/repository/TransactionRepository.java
@@ -1,6 +1,9 @@
package repository;
import data.transaction.TransactionData;
+
+import java.util.List;
+
import data.block.BlockData;
public interface TransactionRepository {
@@ -9,10 +12,29 @@ public interface TransactionRepository {
public TransactionData fromReference(byte[] reference) throws DataException;
+ public TransactionData fromHeightAndSequence(int height, int sequence) throws DataException;
+
public int getHeightFromSignature(byte[] signature) throws DataException;
+ @Deprecated
public BlockData getBlockDataFromSignature(byte[] signature) throws DataException;
+ /**
+ * Returns list of unconfirmed transactions in timestamp-else-signature order.
+ *
+ * @return list of transactions, or empty if none.
+ * @throws DataException
+ */
+ public List getAllUnconfirmedTransactions() throws DataException;
+
+ /**
+ * Remove transaction from unconfirmed transactions pile.
+ *
+ * @param signature
+ * @throws DataException
+ */
+ public void confirmTransaction(byte[] signature) throws DataException;
+
public void save(TransactionData transactionData) throws DataException;
public void delete(TransactionData transactionData) throws DataException;
diff --git a/src/repository/hsqldb/HSQLDBATRepository.java b/src/repository/hsqldb/HSQLDBATRepository.java
index a634fe98..fb7d7a50 100644
--- a/src/repository/hsqldb/HSQLDBATRepository.java
+++ b/src/repository/hsqldb/HSQLDBATRepository.java
@@ -31,7 +31,7 @@ public class HSQLDBATRepository implements ATRepository {
if (resultSet == null)
return null;
- String creator = resultSet.getString(1);
+ byte[] creatorPublicKey = resultSet.getBytes(1);
long creation = resultSet.getTimestamp(2, Calendar.getInstance(HSQLDBRepository.UTC)).getTime();
int version = resultSet.getInt(3);
byte[] codeBytes = resultSet.getBytes(4); // Actually BLOB
@@ -49,18 +49,74 @@ public class HSQLDBATRepository implements ATRepository {
if (resultSet.wasNull())
frozenBalance = null;
- return new ATData(atAddress, creator, creation, version, codeBytes, isSleeping, sleepUntilHeight, isFinished, hadFatalError, isFrozen,
+ return new ATData(atAddress, creatorPublicKey, creation, version, codeBytes, isSleeping, sleepUntilHeight, isFinished, hadFatalError, isFrozen,
frozenBalance);
} catch (SQLException e) {
throw new DataException("Unable to fetch AT from repository", e);
}
}
+ @Override
+ public List getAllExecutableATs() throws DataException {
+ List executableATs = new ArrayList();
+
+ try (ResultSet resultSet = this.repository.checkedExecute(
+ "SELECT AT_address, creator, creation, version, code_bytes, is_sleeping, sleep_until_height, had_fatal_error, is_frozen, frozen_balance FROM ATs WHERE is_finished = false ORDER BY creation ASC")) {
+ if (resultSet == null)
+ return executableATs;
+
+ boolean isFinished = false;
+
+ do {
+ String atAddress = resultSet.getString(1);
+ byte[] creatorPublicKey = resultSet.getBytes(2);
+ long creation = resultSet.getTimestamp(3, Calendar.getInstance(HSQLDBRepository.UTC)).getTime();
+ int version = resultSet.getInt(4);
+ byte[] codeBytes = resultSet.getBytes(5); // Actually BLOB
+ boolean isSleeping = resultSet.getBoolean(6);
+
+ Integer sleepUntilHeight = resultSet.getInt(7);
+ if (resultSet.wasNull())
+ sleepUntilHeight = null;
+
+ boolean hadFatalError = resultSet.getBoolean(8);
+ boolean isFrozen = resultSet.getBoolean(9);
+
+ BigDecimal frozenBalance = resultSet.getBigDecimal(10);
+ if (resultSet.wasNull())
+ frozenBalance = null;
+
+ ATData atData = new ATData(atAddress, creatorPublicKey, creation, version, codeBytes, isSleeping, sleepUntilHeight, isFinished, hadFatalError, isFrozen,
+ frozenBalance);
+
+ executableATs.add(atData);
+ } while (resultSet.next());
+
+ return executableATs;
+ } catch (SQLException e) {
+ throw new DataException("Unable to fetch executable ATs from repository", e);
+ }
+ }
+
+ @Override
+ public Integer getATCreationBlockHeight(String atAddress) throws DataException {
+ try (ResultSet resultSet = this.repository.checkedExecute(
+ "SELECT height from DeployATTransactions JOIN BlockTransactions ON transaction_signature = signature JOIN Blocks ON Blocks.signature = block_signature WHERE AT_address = ?",
+ atAddress)) {
+ if (resultSet == null)
+ return null;
+
+ return resultSet.getInt(1);
+ } catch (SQLException e) {
+ throw new DataException("Unable to fetch AT's creation block height from repository", e);
+ }
+ }
+
@Override
public void save(ATData atData) throws DataException {
HSQLDBSaver saveHelper = new HSQLDBSaver("ATs");
- saveHelper.bind("AT_address", atData.getATAddress()).bind("creator", atData.getCreator()).bind("creation", new Timestamp(atData.getCreation()))
+ saveHelper.bind("AT_address", atData.getATAddress()).bind("creator", atData.getCreatorPublicKey()).bind("creation", new Timestamp(atData.getCreation()))
.bind("version", atData.getVersion()).bind("code_bytes", atData.getCodeBytes()).bind("is_sleeping", atData.getIsSleeping())
.bind("sleep_until_height", atData.getSleepUntilHeight()).bind("is_finished", atData.getIsFinished())
.bind("had_fatal_error", atData.getHadFatalError()).bind("is_frozen", atData.getIsFrozen()).bind("frozen_balance", atData.getFrozenBalance());
@@ -85,7 +141,7 @@ public class HSQLDBATRepository implements ATRepository {
// AT State
@Override
- public ATStateData getATState(String atAddress, int height) throws DataException {
+ public ATStateData getATStateAtHeight(String atAddress, int height) throws DataException {
try (ResultSet resultSet = this.repository
.checkedExecute("SELECT creation, state_data, state_hash, fees FROM ATStates WHERE AT_address = ? AND height = ?", atAddress, height)) {
if (resultSet == null)
@@ -103,7 +159,26 @@ public class HSQLDBATRepository implements ATRepository {
}
@Override
- public List getBlockATStatesFromHeight(int height) throws DataException {
+ public ATStateData getLatestATState(String atAddress) throws DataException {
+ try (ResultSet resultSet = this.repository
+ .checkedExecute("SELECT height, creation, state_data, state_hash, fees FROM ATStates WHERE AT_address = ? ORDER BY height DESC", atAddress)) {
+ if (resultSet == null)
+ return null;
+
+ int height = resultSet.getInt(1);
+ long creation = resultSet.getTimestamp(2, Calendar.getInstance(HSQLDBRepository.UTC)).getTime();
+ byte[] stateData = resultSet.getBytes(3); // Actually BLOB
+ byte[] stateHash = resultSet.getBytes(4);
+ BigDecimal fees = resultSet.getBigDecimal(5);
+
+ return new ATStateData(atAddress, height, creation, stateData, stateHash, fees);
+ } catch (SQLException e) {
+ throw new DataException("Unable to fetch latest AT state from repository", e);
+ }
+ }
+
+ @Override
+ public List getBlockATStatesAtHeight(int height) throws DataException {
List atStates = new ArrayList();
try (ResultSet resultSet = this.repository.checkedExecute("SELECT AT_address, state_hash, fees FROM ATStates WHERE height = ? ORDER BY creation ASC",
diff --git a/src/repository/hsqldb/HSQLDBAccountRepository.java b/src/repository/hsqldb/HSQLDBAccountRepository.java
index 5c716863..6ecc8147 100644
--- a/src/repository/hsqldb/HSQLDBAccountRepository.java
+++ b/src/repository/hsqldb/HSQLDBAccountRepository.java
@@ -72,7 +72,7 @@ public class HSQLDBAccountRepository implements AccountRepository {
@Override
public AccountBalanceData getBalance(String address, long assetId) throws DataException {
- try (ResultSet resultSet = this.repository.checkedExecute("SELECT balance FROM AccountBalances WHERE account = ? and asset_id = ?", address, assetId)) {
+ try (ResultSet resultSet = this.repository.checkedExecute("SELECT balance FROM AccountBalances WHERE account = ? AND asset_id = ?", address, assetId)) {
if (resultSet == null)
return null;
diff --git a/src/repository/hsqldb/HSQLDBDatabaseUpdates.java b/src/repository/hsqldb/HSQLDBDatabaseUpdates.java
index 8d4742cf..40afdce9 100644
--- a/src/repository/hsqldb/HSQLDBDatabaseUpdates.java
+++ b/src/repository/hsqldb/HSQLDBDatabaseUpdates.java
@@ -105,10 +105,11 @@ public class HSQLDBDatabaseUpdates {
case 1:
// Blocks
- stmt.execute("CREATE TABLE Blocks (signature BlockSignature PRIMARY KEY, version TINYINT NOT NULL, reference BlockSignature, "
+ stmt.execute("CREATE TABLE Blocks (signature BlockSignature, version TINYINT NOT NULL, reference BlockSignature, "
+ "transaction_count INTEGER NOT NULL, total_fees QoraAmount NOT NULL, transactions_signature Signature NOT NULL, "
+ "height INTEGER NOT NULL, generation TIMESTAMP WITH TIME ZONE NOT NULL, generating_balance QoraAmount NOT NULL, "
- + "generator QoraPublicKey NOT NULL, generator_signature Signature NOT NULL, AT_count INTEGER NOT NULL, AT_fees QoraAmount NOT NULL)");
+ + "generator QoraPublicKey NOT NULL, generator_signature Signature NOT NULL, AT_count INTEGER NOT NULL, AT_fees QoraAmount NOT NULL, "
+ + "PRIMARY KEY (signature))");
// For finding blocks by height.
stmt.execute("CREATE INDEX BlockHeightIndex ON Blocks (height)");
// For finding blocks by the account that generated them.
@@ -121,30 +122,32 @@ public class HSQLDBDatabaseUpdates {
case 2:
// Generic transactions (null reference, creator and milestone_block for genesis transactions)
- stmt.execute("CREATE TABLE Transactions (signature Signature PRIMARY KEY, reference Signature, type TINYINT NOT NULL, "
- + "creator QoraPublicKey, creation TIMESTAMP WITH TIME ZONE NOT NULL, fee QoraAmount NOT NULL, milestone_block BlockSignature)");
+ stmt.execute("CREATE TABLE Transactions (signature Signature, reference Signature, type TINYINT NOT NULL, "
+ + "creator QoraPublicKey NOT NULL, creation TIMESTAMP WITH TIME ZONE NOT NULL, fee QoraAmount NOT NULL, milestone_block BlockSignature, "
+ + "PRIMARY KEY (signature))");
// For finding transactions by transaction type.
stmt.execute("CREATE INDEX TransactionTypeIndex ON Transactions (type)");
- // For finding transactions using timestamp.
+ // For finding transactions using creation timestamp.
stmt.execute("CREATE INDEX TransactionCreationIndex ON Transactions (creation)");
- // For when a user wants to lookup ALL transactions they have created, regardless of type.
- stmt.execute("CREATE INDEX TransactionCreatorIndex ON Transactions (creator)");
+ // For when a user wants to lookup ALL transactions they have created, with optional type.
+ stmt.execute("CREATE INDEX TransactionCreatorIndex ON Transactions (creator, type)");
// For finding transactions by reference, e.g. child transactions.
stmt.execute("CREATE INDEX TransactionReferenceIndex ON Transactions (reference)");
// Use a separate table space as this table will be very large.
stmt.execute("SET TABLE Transactions NEW SPACE");
- // Transaction-Block mapping ("signature" is unique as a transaction cannot be included in more than one block)
- stmt.execute("CREATE TABLE BlockTransactions (block_signature BlockSignature, sequence INTEGER, transaction_signature Signature, "
+ // Transaction-Block mapping ("transaction_signature" is unique as a transaction cannot be included in more than one block)
+ stmt.execute("CREATE TABLE BlockTransactions (block_signature BlockSignature, sequence INTEGER, transaction_signature Signature UNIQUE, "
+ "PRIMARY KEY (block_signature, sequence), FOREIGN KEY (transaction_signature) REFERENCES Transactions (signature) ON DELETE CASCADE, "
+ "FOREIGN KEY (block_signature) REFERENCES Blocks (signature) ON DELETE CASCADE)");
// Use a separate table space as this table will be very large.
stmt.execute("SET TABLE BlockTransactions NEW SPACE");
// Unconfirmed transactions
- // XXX Do we need this? If a transaction doesn't have a corresponding BlockTransactions record then it's unconfirmed?
- stmt.execute("CREATE TABLE UnconfirmedTransactions (signature Signature PRIMARY KEY, expiry TIMESTAMP WITH TIME ZONE NOT NULL)");
- stmt.execute("CREATE INDEX UnconfirmedTransactionExpiryIndex ON UnconfirmedTransactions (expiry)");
+ // We use this as searching for transactions with no corresponding mapping in BlockTransactions is much slower.
+ stmt.execute("CREATE TABLE UnconfirmedTransactions (signature Signature PRIMARY KEY, creation TIMESTAMP WITH TIME ZONE NOT NULL)");
+ // Index to allow quick sorting by creation-else-signature
+ stmt.execute("CREATE INDEX UnconfirmedTransactionsIndex ON UnconfirmedTransactions (creation, signature)");
// Transaction recipients
stmt.execute("CREATE TABLE TransactionRecipients (signature Signature, recipient QoraAddress NOT NULL, "
@@ -276,6 +279,8 @@ public class HSQLDBDatabaseUpdates {
+ "description VARCHAR(2000) NOT NULL, AT_type ATType NOT NULL, AT_tags VARCHAR(200) NOT NULL, "
+ "creation_bytes VARBINARY(100000) NOT NULL, amount QoraAmount NOT NULL, AT_address QoraAddress, "
+ "PRIMARY KEY (signature), FOREIGN KEY (signature) REFERENCES Transactions (signature) ON DELETE CASCADE)");
+ // For looking up the Deploy AT Transaction based on deployed AT address
+ stmt.execute("CREATE INDEX DeployATAddressIndex on DeployATTransactions (AT_address)");
break;
case 20:
@@ -353,22 +358,29 @@ public class HSQLDBDatabaseUpdates {
case 27:
// CIYAM Automated Transactions
- stmt.execute("CREATE TABLE ATs (AT_address QoraAddress, creator QoraAddress, creation TIMESTAMP WITH TIME ZONE, version INTEGER NOT NULL, "
- + "code_bytes ATCode NOT NULL, is_sleeping BOOLEAN NOT NULL, sleep_until_height INTEGER, "
- + "is_finished BOOLEAN NOT NULL, had_fatal_error BOOLEAN NOT NULL, is_frozen BOOLEAN NOT NULL, frozen_balance QoraAmount, "
- + "PRIMARY key (AT_address))");
+ stmt.execute(
+ "CREATE TABLE ATs (AT_address QoraAddress, creator QoraPublicKey, creation TIMESTAMP WITH TIME ZONE, version INTEGER NOT NULL, "
+ + "code_bytes ATCode NOT NULL, is_sleeping BOOLEAN NOT NULL, sleep_until_height INTEGER, "
+ + "is_finished BOOLEAN NOT NULL, had_fatal_error BOOLEAN NOT NULL, is_frozen BOOLEAN NOT NULL, frozen_balance QoraAmount, "
+ + "PRIMARY key (AT_address))");
// For finding executable ATs, ordered by creation timestamp
- stmt.execute("CREATE INDEX ATIndex on ATs (is_finished, creation, AT_address)");
+ stmt.execute("CREATE INDEX ATIndex on ATs (is_finished, creation)");
+ // For finding ATs by creator
+ stmt.execute("CREATE INDEX ATCreatorIndex on ATs (creator)");
+
// AT state on a per-block basis
stmt.execute("CREATE TABLE ATStates (AT_address QoraAddress, height INTEGER NOT NULL, creation TIMESTAMP WITH TIME ZONE, "
+ "state_data ATState, state_hash ATStateHash NOT NULL, fees QoraAmount NOT NULL, "
+ "PRIMARY KEY (AT_address, height), FOREIGN KEY (AT_address) REFERENCES ATs (AT_address) ON DELETE CASCADE)");
// For finding per-block AT states, ordered by creation timestamp
- stmt.execute("CREATE INDEX BlockATStateIndex on ATStates (height, creation, AT_address)");
+ stmt.execute("CREATE INDEX BlockATStateIndex on ATStates (height, creation)");
+
// Generated AT Transactions
stmt.execute(
"CREATE TABLE ATTransactions (signature Signature, AT_address QoraAddress NOT NULL, recipient QoraAddress, amount QoraAmount, asset_id AssetID, message ATMessage, "
+ "PRIMARY KEY (signature), FOREIGN KEY (signature) REFERENCES Transactions (signature) ON DELETE CASCADE)");
+ // For finding AT Transactions generated by a specific AT
+ stmt.execute("CREATE INDEX ATTransactionsIndex on ATTransactions (AT_address)");
break;
default:
diff --git a/src/repository/hsqldb/HSQLDBVotingRepository.java b/src/repository/hsqldb/HSQLDBVotingRepository.java
index cedb1c62..232a63aa 100644
--- a/src/repository/hsqldb/HSQLDBVotingRepository.java
+++ b/src/repository/hsqldb/HSQLDBVotingRepository.java
@@ -35,7 +35,7 @@ public class HSQLDBVotingRepository implements VotingRepository {
long published = resultSet.getTimestamp(4, Calendar.getInstance(HSQLDBRepository.UTC)).getTime();
try (ResultSet optionsResultSet = this.repository
- .checkedExecute("SELECT option_name FROM PollOptions where poll_name = ? ORDER BY option_index ASC", pollName)) {
+ .checkedExecute("SELECT option_name FROM PollOptions WHERE poll_name = ? ORDER BY option_index ASC", pollName)) {
if (optionsResultSet == null)
return null;
diff --git a/src/repository/hsqldb/transaction/HSQLDBCreatePollTransactionRepository.java b/src/repository/hsqldb/transaction/HSQLDBCreatePollTransactionRepository.java
index 08d8dc4e..ad6733f6 100644
--- a/src/repository/hsqldb/transaction/HSQLDBCreatePollTransactionRepository.java
+++ b/src/repository/hsqldb/transaction/HSQLDBCreatePollTransactionRepository.java
@@ -30,7 +30,7 @@ public class HSQLDBCreatePollTransactionRepository extends HSQLDBTransactionRepo
String description = resultSet.getString(3);
try (ResultSet optionsResultSet = this.repository
- .checkedExecute("SELECT option_name FROM CreatePollTransactionOptions where signature = ? ORDER BY option_index ASC", signature)) {
+ .checkedExecute("SELECT option_name FROM CreatePollTransactionOptions WHERE signature = ? ORDER BY option_index ASC", signature)) {
if (optionsResultSet == null)
return null;
diff --git a/src/repository/hsqldb/transaction/HSQLDBTransactionRepository.java b/src/repository/hsqldb/transaction/HSQLDBTransactionRepository.java
index 8360274a..69e3f112 100644
--- a/src/repository/hsqldb/transaction/HSQLDBTransactionRepository.java
+++ b/src/repository/hsqldb/transaction/HSQLDBTransactionRepository.java
@@ -102,6 +102,22 @@ public class HSQLDBTransactionRepository implements TransactionRepository {
}
}
+ @Override
+ public TransactionData fromHeightAndSequence(int height, int sequence) throws DataException {
+ try (ResultSet resultSet = this.repository.checkedExecute(
+ "SELECT transaction_signature FROM BlockTransactions JOIN Blocks ON signature = block_signature WHERE height = ? AND sequence = ?", height,
+ sequence)) {
+ if (resultSet == null)
+ return null;
+
+ byte[] signature = resultSet.getBytes(1);
+
+ return this.fromSignature(signature);
+ } catch (SQLException e) {
+ throw new DataException("Unable to fetch transaction from repository", e);
+ }
+ }
+
private TransactionData fromBase(TransactionType type, byte[] signature, byte[] reference, byte[] creatorPublicKey, long timestamp, BigDecimal fee)
throws DataException {
switch (type) {
@@ -236,7 +252,7 @@ public class HSQLDBTransactionRepository implements TransactionRepository {
return null;
// Fetch block signature (if any)
- try (ResultSet resultSet = this.repository.checkedExecute("SELECT block_signature from BlockTransactions WHERE transaction_signature = ? LIMIT 1",
+ try (ResultSet resultSet = this.repository.checkedExecute("SELECT block_signature FROM BlockTransactions WHERE transaction_signature = ? LIMIT 1",
signature)) {
if (resultSet == null)
return null;
@@ -249,6 +265,42 @@ public class HSQLDBTransactionRepository implements TransactionRepository {
}
}
+ @Override
+ public List getAllUnconfirmedTransactions() throws DataException {
+ List transactions = new ArrayList();
+
+ // Find transactions with no corresponding row in BlockTransactions
+ try (ResultSet resultSet = this.repository.checkedExecute("SELECT signature FROM UnconfirmedTransactions ORDER BY creation ASC, signature ASC")) {
+ if (resultSet == null)
+ return transactions;
+
+ do {
+ byte[] signature = resultSet.getBytes(1);
+
+ TransactionData transactionData = this.fromSignature(signature);
+
+ if (transactionData == null)
+ // Something inconsistent with the repository
+ throw new DataException("Unable to fetch unconfirmed transaction from repository?");
+
+ transactions.add(transactionData);
+ } while (resultSet.next());
+
+ return transactions;
+ } catch (SQLException | DataException e) {
+ throw new DataException("Unable to fetch unconfirmed transactions from repository", e);
+ }
+ }
+
+ @Override
+ public void confirmTransaction(byte[] signature) throws DataException {
+ try {
+ this.repository.delete("UnconfirmedTransactions", "signature = ?", signature);
+ } catch (SQLException e) {
+ throw new DataException("Unable to remove transaction from unconfirmed transactions repository", e);
+ }
+ }
+
@Override
public void save(TransactionData transactionData) throws DataException {
HSQLDBSaver saver = new HSQLDBSaver("Transactions");
diff --git a/src/test/SignatureTests.java b/src/test/SignatureTests.java
index bc15ee14..83a05fc6 100644
--- a/src/test/SignatureTests.java
+++ b/src/test/SignatureTests.java
@@ -34,20 +34,30 @@ public class SignatureTests extends Common {
@Test
public void testBlockSignature() throws DataException {
- int version = 3;
-
- byte[] reference = Base58.decode(
- "BSfgEr6r1rXGGJCv8criR5NcBWfpHdJnm9x5unPwxvojEKCESv1wH1zJm7yvCeC48wshymYtARbHdUojbqWCCWW7h2UTc8g5oEx59C9M41dM7H48My8gVkcEZdxR1of3VgpE5UcowFp3kFC12hVcD9hUttJ2i2nZWMwprbFtUGyVv1U");
-
- long timestamp = NTP.getTime() - 5000;
-
- BigDecimal generatingBalance = BigDecimal.valueOf(10_000_000L).setScale(8);
-
try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount generator = new PrivateKeyAccount(repository,
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 });
- Block block = new Block(repository, version, reference, timestamp, generatingBalance, generator);
+ int version = 3;
+
+ byte[] reference = Base58.decode(
+ "BSfgEr6r1rXGGJCv8criR5NcBWfpHdJnm9x5unPwxvojEKCESv1wH1zJm7yvCeC48wshymYtARbHdUojbqWCCWW7h2UTc8g5oEx59C9M41dM7H48My8gVkcEZdxR1of3VgpE5UcowFp3kFC12hVcD9hUttJ2i2nZWMwprbFtUGyVv1U");
+
+ int transactionCount = 0;
+ BigDecimal totalFees = BigDecimal.ZERO.setScale(8);
+ byte[] transactionsSignature = null;
+ int height = 0;
+ long timestamp = NTP.getTime() - 5000;
+ BigDecimal generatingBalance = BigDecimal.valueOf(10_000_000L).setScale(8);
+ byte[] generatorPublicKey = generator.getPublicKey();
+ byte[] generatorSignature = null;
+ int atCount = 0;
+ BigDecimal atFees = BigDecimal.valueOf(10_000_000L).setScale(8);
+
+ BlockData blockData = new BlockData(version, reference, transactionCount, totalFees, transactionsSignature, height, timestamp, generatingBalance,
+ generatorPublicKey, generatorSignature, atCount, atFees);
+
+ Block block = new Block(repository, blockData, generator);
block.sign();
assertTrue(block.isSignatureValid());
diff --git a/src/transform/block/BlockTransformer.java b/src/transform/block/BlockTransformer.java
index 5bc5796e..f73471ac 100644
--- a/src/transform/block/BlockTransformer.java
+++ b/src/transform/block/BlockTransformer.java
@@ -310,7 +310,7 @@ public class BlockTransformer extends Transformer {
Order order = orderTransaction.getOrder();
List trades = order.getTrades();
- // Filter out trades with initiatingOrderId that doesn't match this order
+ // Filter out trades with initiatingOrderId that don't match this order
trades.removeIf((TradeData tradeData) -> !Arrays.equals(tradeData.getInitiator(), order.getOrderData().getOrderId()));
// Any trades left?
diff --git a/src/transform/transaction/ArbitraryTransactionTransformer.java b/src/transform/transaction/ArbitraryTransactionTransformer.java
index 5d408a34..fac8f34e 100644
--- a/src/transform/transaction/ArbitraryTransactionTransformer.java
+++ b/src/transform/transaction/ArbitraryTransactionTransformer.java
@@ -48,12 +48,12 @@ public class ArbitraryTransactionTransformer extends TransactionTransformer {
byte[] senderPublicKey = Serialization.deserializePublicKey(byteBuffer);
- // V3+ allows payments
- List payments = null;
+ // V3+ allows payments but always return a list of payments, even if empty
+ List payments = new ArrayList();
+ ;
if (version != 1) {
int paymentsCount = byteBuffer.getInt();
- payments = new ArrayList();
for (int i = 0; i < paymentsCount; ++i)
payments.add(PaymentTransformer.fromByteBuffer(byteBuffer));
}