diff --git a/src/main/java/api/ApiError.java b/src/main/java/api/ApiError.java
index 73b19c2a..4c3b12a6 100644
--- a/src/main/java/api/ApiError.java
+++ b/src/main/java/api/ApiError.java
@@ -42,6 +42,7 @@ public enum ApiError {
INVALID_CRITERIA(125, 400),
INVALID_REFERENCE(126, 400),
TRANSFORMATION_ERROR(127, 400),
+ INVALID_PRIVATE_KEY(128, 400),
// WALLET
WALLET_NO_EXISTS(201, 404),
diff --git a/src/main/java/api/ApiExceptionFactory.java b/src/main/java/api/ApiExceptionFactory.java
index d5156148..0d5a80ef 100644
--- a/src/main/java/api/ApiExceptionFactory.java
+++ b/src/main/java/api/ApiExceptionFactory.java
@@ -8,8 +8,7 @@ public enum ApiExceptionFactory {
INSTANCE;
public ApiException createException(HttpServletRequest request, ApiError apiError, Throwable throwable, Object... args) {
- String template = Translator.INSTANCE.translate("ApiError", request.getLocale().getLanguage(), apiError.name());
- String message = String.format(template, args);
+ String message = Translator.INSTANCE.translate("ApiError", request.getLocale().getLanguage(), apiError.name(), args);
return new ApiException(apiError.getStatus(), apiError.getCode(), message, throwable);
}
diff --git a/src/main/java/api/resource/AddressesResource.java b/src/main/java/api/resource/AddressesResource.java
index cfc0a4d1..2d705b3e 100644
--- a/src/main/java/api/resource/AddressesResource.java
+++ b/src/main/java/api/resource/AddressesResource.java
@@ -44,40 +44,6 @@ public class AddressesResource {
@GET
@Path("/lastreference/{address}")
- @Operation(
- summary = "Fetch reference for next transaction to be created by address",
- description = "Returns the base58-encoded signature of the last confirmed transaction created by address, failing that: the first incoming transaction to address. Returns \"false\" if there is no transactions.",
- responses = {
- @ApiResponse(
- description = "the base58-encoded transaction signature or \"false\"",
- content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(type = "string"))
- )
- }
- )
- @ApiErrors({ApiError.INVALID_ADDRESS, ApiError.REPOSITORY_ISSUE})
- public String getLastReference(@Parameter(ref = "address") @PathParam("address") String address) {
- if (!Crypto.isValidAddress(address))
- throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS);
-
- byte[] lastReference = null;
- try (final Repository repository = RepositoryManager.getRepository()) {
- Account account = new Account(repository, address);
- lastReference = account.getLastReference();
- } catch (ApiException e) {
- throw e;
- } catch (DataException e) {
- throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
- }
-
- if(lastReference == null || lastReference.length == 0) {
- return "false";
- } else {
- return Base58.encode(lastReference);
- }
- }
-
- @GET
- @Path("/lastreference/{address}/unconfirmed")
@Operation(
summary = "Fetch reference for next transaction to be created by address, considering unconfirmed transactions",
description = "Returns the base58-encoded signature of the last confirmed/unconfirmed transaction created by address, failing that: the first incoming transaction. Returns \"false\" if there is no transactions.",
diff --git a/src/main/java/api/resource/TransactionsResource.java b/src/main/java/api/resource/TransactionsResource.java
index 901e9132..06c8f32d 100644
--- a/src/main/java/api/resource/TransactionsResource.java
+++ b/src/main/java/api/resource/TransactionsResource.java
@@ -261,8 +261,11 @@ public class TransactionsResource {
)
}
)
- @ApiErrors({ApiError.TRANSFORMATION_ERROR})
+ @ApiErrors({ApiError.INVALID_PRIVATE_KEY, ApiError.TRANSFORMATION_ERROR})
public String signTransaction(SimpleTransactionSignRequest signRequest) {
+ if (signRequest.transactionBytes.length == 0)
+ throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.JSON);
+
try {
// Append null signature on the end before transformation
byte[] rawBytes = Bytes.concat(signRequest.transactionBytes, new byte[TransactionTransformer.SIGNATURE_LENGTH]);
@@ -277,6 +280,9 @@ public class TransactionsResource {
byte[] signedBytes = TransactionTransformer.toBytes(transactionData);
return Base58.encode(signedBytes);
+ } catch (IllegalArgumentException e) {
+ // Invalid private key
+ throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PRIVATE_KEY);
} catch (TransformationException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.TRANSFORMATION_ERROR, e);
}
@@ -328,6 +334,8 @@ public class TransactionsResource {
repository.saveChanges();
return "true";
+ } catch (NumberFormatException e) {
+ throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA, e);
} catch (TransformationException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.TRANSFORMATION_ERROR, e);
} catch (ApiException e) {
@@ -391,6 +399,8 @@ public class TransactionsResource {
transactionData.setSignature(null);
return transactionData;
+ } catch (NumberFormatException e) {
+ throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA, e);
} catch (TransformationException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.TRANSFORMATION_ERROR, e);
} catch (ApiException e) {
diff --git a/src/main/java/globalization/Translator.java b/src/main/java/globalization/Translator.java
index 15291f91..9bdd1462 100644
--- a/src/main/java/globalization/Translator.java
+++ b/src/main/java/globalization/Translator.java
@@ -3,6 +3,7 @@ package globalization;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
+import java.util.MissingFormatArgumentException;
import java.util.MissingResourceException;
import java.util.ResourceBundle;
@@ -46,7 +47,12 @@ public enum Translator {
if (resourceBundle == null || !resourceBundle.containsKey(key))
return "!!" + lang + ":" + className + "." + key + "!!";
- return String.format(resourceBundle.getString(key), args);
+ String template = resourceBundle.getString(key);
+ try {
+ return String.format(template, args);
+ } catch (MissingFormatArgumentException e) {
+ return template;
+ }
}
}
diff --git a/src/main/java/qora/account/Account.java b/src/main/java/qora/account/Account.java
index 32bf44f6..4473ea5d 100644
--- a/src/main/java/qora/account/Account.java
+++ b/src/main/java/qora/account/Account.java
@@ -149,13 +149,15 @@ public class Account {
/**
* Fetch last reference for account, considering unconfirmed transactions.
+ *
+ * NOTE: repository.discardChanges() may be called during execution.
*
* @return byte[] reference, or null if no reference or account not found.
* @throws DataException
*/
public byte[] getUnconfirmedLastReference() throws DataException {
// Newest unconfirmed transaction takes priority
- List unconfirmedTransactions = repository.getTransactionRepository().getAllUnconfirmedTransactions();
+ List unconfirmedTransactions = Transaction.getUnconfirmedTransactions(repository);
byte[] reference = null;
diff --git a/src/main/java/qora/account/PrivateKeyAccount.java b/src/main/java/qora/account/PrivateKeyAccount.java
index 2eec5f7a..cdf06b67 100644
--- a/src/main/java/qora/account/PrivateKeyAccount.java
+++ b/src/main/java/qora/account/PrivateKeyAccount.java
@@ -16,6 +16,7 @@ public class PrivateKeyAccount extends PublicKeyAccount {
*
* @param seed
* byte[32] used to create private/public key pair
+ * @throws IllegalArgumentException if passed invalid seed
*/
public PrivateKeyAccount(Repository repository, byte[] seed) {
this.repository = repository;
diff --git a/src/main/java/qora/block/BlockGenerator.java b/src/main/java/qora/block/BlockGenerator.java
index dd406c41..14b349a7 100644
--- a/src/main/java/qora/block/BlockGenerator.java
+++ b/src/main/java/qora/block/BlockGenerator.java
@@ -9,13 +9,14 @@ import org.apache.logging.log4j.Logger;
import data.block.BlockData;
import data.transaction.TransactionData;
import qora.account.PrivateKeyAccount;
-import qora.account.PublicKeyAccount;
import qora.block.Block.ValidationResult;
import qora.transaction.Transaction;
import repository.BlockRepository;
import repository.DataException;
import repository.Repository;
import repository.RepositoryManager;
+import settings.Settings;
+import utils.Base58;
// Forging new blocks
@@ -48,11 +49,17 @@ public class BlockGenerator extends Thread {
Thread.currentThread().setName("BlockGenerator");
try (final Repository repository = RepositoryManager.getRepository()) {
- // Wipe existing unconfirmed transactions
- List unconfirmedTransactions = repository.getTransactionRepository().getAllUnconfirmedTransactions();
- for (TransactionData transactionData : unconfirmedTransactions)
- repository.getTransactionRepository().delete(transactionData);
- repository.saveChanges();
+ if (Settings.getInstance().getWipeUnconfirmedOnStart()) {
+ // Wipe existing unconfirmed transactions
+ List unconfirmedTransactions = repository.getTransactionRepository().getAllUnconfirmedTransactions();
+
+ for (TransactionData transactionData : unconfirmedTransactions) {
+ LOGGER.trace(String.format("Deleting unconfirmed transaction %s", Base58.encode(transactionData.getSignature())));
+ repository.getTransactionRepository().delete(transactionData);
+ }
+
+ repository.saveChanges();
+ }
generator = new PrivateKeyAccount(repository, generatorPrivateKey);
@@ -101,7 +108,8 @@ public class BlockGenerator extends Thread {
// Sleep for a while
try {
- Thread.sleep(1000);
+ repository.discardChanges(); // Free transactional locks, if any
+ Thread.sleep(1000); // No point sleeping less than this as block timestamp millisecond values must be the same
} catch (InterruptedException e) {
// We've been interrupted - time to exit
return;
@@ -113,10 +121,28 @@ public class BlockGenerator extends Thread {
}
private void addUnconfirmedTransactions(Repository repository, Block newBlock) throws DataException {
- // Grab all unconfirmed transactions (already sorted)
- List unconfirmedTransactions = repository.getTransactionRepository().getAllUnconfirmedTransactions();
+ // Grab all valid unconfirmed transactions (already sorted)
+ List unconfirmedTransactions = Transaction.getUnconfirmedTransactions(repository);
- unconfirmedTransactions.removeIf(transactionData -> !isSuitableTransaction(repository, transactionData, newBlock));
+ for (int i = 0; i < unconfirmedTransactions.size(); ++i) {
+ TransactionData transactionData = unconfirmedTransactions.get(i);
+
+ // Ignore transactions that have timestamp later than block's timestamp (not yet valid)
+ if (transactionData.getTimestamp() > newBlock.getBlockData().getTimestamp()) {
+ unconfirmedTransactions.remove(i);
+ --i;
+ continue;
+ }
+
+ Transaction transaction = Transaction.fromData(repository, transactionData);
+
+ // Ignore transactions that have expired before this block - they will be cleaned up later
+ if (transaction.getDeadline() <= newBlock.getBlockData().getTimestamp()) {
+ unconfirmedTransactions.remove(i);
+ --i;
+ continue;
+ }
+ }
// Discard last-reference changes used to aid transaction validity checks
repository.discardChanges();
@@ -127,40 +153,6 @@ public class BlockGenerator extends Thread {
break;
}
- /** Returns true if transaction is suitable for adding to new block */
- private boolean isSuitableTransaction(Repository repository, TransactionData transactionData, Block newBlock) {
- // Ignore transactions that have timestamp later than block's timestamp (not yet valid)
- if (transactionData.getTimestamp() > newBlock.getBlockData().getTimestamp())
- return false;
-
- Transaction transaction = Transaction.fromData(repository, transactionData);
-
- // Ignore transactions that have expired deadline for this block
- if (transaction.getDeadline() <= newBlock.getBlockData().getTimestamp())
- return false;
-
- // Ignore transactions that are currently not valid
- try {
- if (transaction.isValid() != Transaction.ValidationResult.OK)
- return false;
- } catch (DataException e) {
- // Not good either
- return false;
- }
-
- // Good for adding to a block
- // Temporarily update sender's last reference so that subsequent transactions validations work
- // These updates will be discard on exit of addUnconfirmedTransactions() above
- PublicKeyAccount creator = new PublicKeyAccount(repository, transactionData.getCreatorPublicKey());
- try {
- creator.setLastReference(transactionData.getSignature());
- } catch (DataException e) {
- // Not good
- return false;
- }
- return true;
- }
-
public void shutdown() {
this.running = false;
// Interrupt too, absorbed by HSQLDB but could be caught by Thread.sleep()
diff --git a/src/main/java/qora/transaction/Transaction.java b/src/main/java/qora/transaction/Transaction.java
index 6013b7f9..6a547391 100644
--- a/src/main/java/qora/transaction/Transaction.java
+++ b/src/main/java/qora/transaction/Transaction.java
@@ -8,9 +8,13 @@ import java.util.Comparator;
import java.util.List;
import java.util.Map;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
import static java.util.Arrays.stream;
import static java.util.stream.Collectors.toMap;
+import data.block.BlockData;
import data.transaction.TransactionData;
import qora.account.Account;
import qora.account.PrivateKeyAccount;
@@ -20,6 +24,8 @@ import repository.DataException;
import repository.Repository;
import transform.TransformationException;
import transform.transaction.TransactionTransformer;
+import utils.Base58;
+import utils.NTP;
public abstract class Transaction {
@@ -118,6 +124,8 @@ public abstract class Transaction {
}
}
+ private static final Logger LOGGER = LogManager.getLogger(Transaction.class);
+
// Properties
protected Repository repository;
protected TransactionData transactionData;
@@ -432,6 +440,78 @@ public abstract class Transaction {
}
}
+ /**
+ * Returns sorted, unconfirmed transactions, deleting invalid.
+ *
+ * NOTE: temporarily updates accounts' lastReference to that from
+ * unconfirmed transactions, and hence calls repository.discardChanges()
+ * before exit.
+ *
+ * @return sorted unconfirmed transactions
+ * @throws DataException
+ */
+ public static List getUnconfirmedTransactions(Repository repository) throws DataException {
+ BlockData latestBlockData = repository.getBlockRepository().getLastBlock();
+
+ List unconfirmedTransactions = repository.getTransactionRepository().getAllUnconfirmedTransactions();
+ List invalidTransactions = new ArrayList<>();
+
+ unconfirmedTransactions.sort(getDataComparator());
+
+ for (int i = 0; i < unconfirmedTransactions.size(); ++i) {
+ TransactionData transactionData = unconfirmedTransactions.get(i);
+
+ if (!isStillValidUnconfirmed(repository, transactionData, latestBlockData.getTimestamp())) {
+ invalidTransactions.add(transactionData);
+
+ unconfirmedTransactions.remove(i);
+ --i;
+ continue;
+ }
+ }
+
+ // Throw away temporary updates to account lastReference
+ repository.discardChanges();
+
+ // Actually delete invalid transactions from database
+ for (TransactionData invalidTransactionData : invalidTransactions) {
+ LOGGER.trace(String.format("Deleting invalid, unconfirmed transaction %s", Base58.encode(invalidTransactionData.getSignature())));
+ repository.getTransactionRepository().delete(invalidTransactionData);
+ }
+ repository.saveChanges();
+
+ return unconfirmedTransactions;
+ }
+
+ /**
+ * Returns whether transaction is still a valid unconfirmed transaction.
+ *
+ * NOTE: temporarily updates creator's lastReference to that from
+ * unconfirmed transactions, and hence caller should invoke
+ * repository.discardChanges().
+ *
+ * @return true if transaction can be added to unconfirmed transactions, false otherwise
+ * @throws DataException
+ */
+ private static boolean isStillValidUnconfirmed(Repository repository, TransactionData transactionData, long blockTimestamp) throws DataException {
+ Transaction transaction = Transaction.fromData(repository, transactionData);
+
+ // Check transaction has not expired
+ if (transaction.getDeadline() <= blockTimestamp || transaction.getDeadline() < NTP.getTime())
+ return false;
+
+ // Check transaction is currently valid
+ if (transaction.isValid() != Transaction.ValidationResult.OK)
+ return false;
+
+ // Good for adding to a block
+ // Temporarily update sender's last reference so that subsequent transactions validations work
+ // These updates should be discarded by some caller further up stack
+ PublicKeyAccount creator = new PublicKeyAccount(repository, transactionData.getCreatorPublicKey());
+ creator.setLastReference(transactionData.getSignature());
+ return true;
+ }
+
/**
* Returns whether transaction can be added to the blockchain.
*
@@ -467,12 +547,32 @@ public abstract class Transaction {
public static Comparator getComparator() {
class TransactionComparator implements Comparator {
+ private Comparator transactionDataComparator;
+
+ public TransactionComparator(Comparator transactionDataComparator) {
+ this.transactionDataComparator = transactionDataComparator;
+ }
+
// Compare by type, timestamp, then signature
@Override
public int compare(Transaction t1, Transaction t2) {
TransactionData td1 = t1.getTransactionData();
TransactionData td2 = t2.getTransactionData();
+ return transactionDataComparator.compare(td1, td2);
+ }
+
+ }
+
+ return new TransactionComparator(getDataComparator());
+ }
+
+ public static Comparator getDataComparator() {
+ class TransactionDataComparator implements Comparator {
+
+ // Compare by type, timestamp, then signature
+ @Override
+ public int compare(TransactionData td1, TransactionData td2) {
// AT transactions come before non-AT transactions
if (td1.getType() == TransactionType.AT && td2.getType() != TransactionType.AT)
return -1;
@@ -492,7 +592,7 @@ public abstract class Transaction {
}
- return new TransactionComparator();
+ return new TransactionDataComparator();
}
@Override
diff --git a/src/main/java/settings/Settings.java b/src/main/java/settings/Settings.java
index e9d75541..71724575 100644
--- a/src/main/java/settings/Settings.java
+++ b/src/main/java/settings/Settings.java
@@ -27,6 +27,7 @@ public class Settings {
private static Settings instance;
private String userpath = "";
private boolean useBitcoinTestNet = false;
+ private boolean wipeUnconfirmedOnStart = true;
// RPC
private int rpcPort = 9085;
@@ -130,6 +131,12 @@ public class Settings {
if (json.containsKey("rpcenabled"))
this.rpcEnabled = ((Boolean) json.get("rpcenabled")).booleanValue();
+ // Blockchain config
+
+ if (json.containsKey("wipeUnconfirmedOnStart")) {
+ this.wipeUnconfirmedOnStart = (Boolean) getTypedJson(json, "wipeUnconfirmedOnStart", Boolean.class);
+ }
+
if (json.containsKey("blockchainConfig")) {
String filename = (String) json.get("blockchainConfig");
File file = new File(this.userpath + filename);
@@ -172,6 +179,10 @@ public class Settings {
return this.useBitcoinTestNet;
}
+ public boolean getWipeUnconfirmedOnStart() {
+ return this.wipeUnconfirmedOnStart;
+ }
+
// Config parsing
public static Object getTypedJson(JSONObject json, String key, Class> clazz) {
diff --git a/src/main/resources/i18n/ApiError_en.properties b/src/main/resources/i18n/ApiError_en.properties
index efc816fc..b54bc1bc 100644
--- a/src/main/resources/i18n/ApiError_en.properties
+++ b/src/main/resources/i18n/ApiError_en.properties
@@ -33,6 +33,8 @@ WALLET_NOT_IN_SYNC=wallet needs to be synchronized
INVALID_NETWORK_ADDRESS=invalid network address
ADDRESS_NO_EXISTS=account address does not exist
INVALID_CRITERIA=invalid search criteria
+INVALID_REFERENCE=invalid reference
+INVALID_PRIVATE_KEY=invalid private key
# Wallet
WALLET_NO_EXISTS=wallet does not exist
@@ -47,7 +49,7 @@ BLOCK_NO_EXISTS=block does not exist
# Transactions
TRANSACTION_NO_EXISTS=transaction does not exist
PUBLIC_KEY_NOT_FOUND=public key not found
-TRANSACTION_INVALID=transaction invalid
+TRANSACTION_INVALID=transaction invalid: %s
# Names
NAME_NO_EXISTS=name does not exist