From 7a53ac17a62b1363f5f32e8778a362edd8bdeee9 Mon Sep 17 00:00:00 2001
From: catbref <misc-github@talk2dom.com>
Date: Mon, 11 Feb 2019 17:37:52 +0000
Subject: [PATCH] Synchronization, peer management + fixes

Peers now broadcast height after successful synchronization.

Added support for sending unconfirmed transactions to other peers.
This is done on connect and also after a new unconfirmed transaction is submitted via API.

Fixed synchronizer to handle blocks with transactions correctly.

Fixed network-related PoW to not use class-global SHA256 message digester!
(It was being corrupted by simulataneous access by different threads - whoops)

Surrounded Network.mergePeers with a lock to prevent HSQLDB deadlocks.
Also changed HSQLDB concurrency model to MVCC (only takes effect if DB rebuilt).

Added support for logging other HSQLDB sessions in the event of exception.
(Currently only used by HSQLDBSaver)

Transaction transformer modifications to help deserialize TransactionMessages.
---
 .../api/resource/TransactionsResource.java    |  4 +
 .../java/org/qora/controller/Controller.java  | 82 +++++++++++++++----
 .../org/qora/controller/Synchronizer.java     | 19 +++--
 src/main/java/org/qora/network/Handshake.java |  2 +-
 src/main/java/org/qora/network/Network.java   | 75 +++++++++++++----
 src/main/java/org/qora/network/Peer.java      | 12 ++-
 src/main/java/org/qora/network/Proof.java     | 28 ++++---
 .../network/message/TransactionMessage.java   | 50 +++++++++++
 .../repository/TransactionRepository.java     |  2 +
 .../hsqldb/HSQLDBDatabaseUpdates.java         |  1 +
 .../repository/hsqldb/HSQLDBRepository.java   | 31 ++++++-
 .../qora/repository/hsqldb/HSQLDBSaver.java   |  2 +
 .../HSQLDBTransactionRepository.java          |  9 ++
 .../AddGroupAdminTransactionTransformer.java  |  2 +-
 .../ArbitraryTransactionTransformer.java      |  2 +-
 .../transaction/AtTransactionTransformer.java |  2 +-
 .../BuyNameTransactionTransformer.java        |  2 +-
 ...ancelAssetOrderTransactionTransformer.java |  2 +-
 .../CancelGroupBanTransactionTransformer.java |  2 +-
 ...ncelGroupInviteTransactionTransformer.java |  2 +-
 .../CancelSellNameTransactionTransformer.java |  2 +-
 ...reateAssetOrderTransactionTransformer.java |  2 +-
 .../CreateGroupTransactionTransformer.java    |  2 +-
 .../CreatePollTransactionTransformer.java     |  2 +-
 .../DeployAtTransactionTransformer.java       |  2 +-
 .../GenesisTransactionTransformer.java        |  2 +-
 .../GroupBanTransactionTransformer.java       |  2 +-
 .../GroupInviteTransactionTransformer.java    |  2 +-
 .../GroupKickTransactionTransformer.java      |  2 +-
 .../IssueAssetTransactionTransformer.java     |  2 +-
 .../JoinGroupTransactionTransformer.java      |  2 +-
 .../LeaveGroupTransactionTransformer.java     |  2 +-
 .../MessageTransactionTransformer.java        |  2 +-
 .../MultiPaymentTransactionTransformer.java   |  2 +-
 .../PaymentTransactionTransformer.java        |  2 +-
 .../RegisterNameTransactionTransformer.java   |  2 +-
 ...emoveGroupAdminTransactionTransformer.java |  2 +-
 .../SellNameTransactionTransformer.java       |  2 +-
 .../transaction/TransactionTransformer.java   | 10 ++-
 .../TransferAssetTransactionTransformer.java  |  2 +-
 .../UpdateGroupTransactionTransformer.java    |  2 +-
 .../UpdateNameTransactionTransformer.java     |  2 +-
 .../VoteOnPollTransactionTransformer.java     |  2 +-
 43 files changed, 298 insertions(+), 87 deletions(-)
 create mode 100644 src/main/java/org/qora/network/message/TransactionMessage.java

diff --git a/src/main/java/org/qora/api/resource/TransactionsResource.java b/src/main/java/org/qora/api/resource/TransactionsResource.java
index 278c255d..2fc00393 100644
--- a/src/main/java/org/qora/api/resource/TransactionsResource.java
+++ b/src/main/java/org/qora/api/resource/TransactionsResource.java
@@ -28,6 +28,7 @@ import org.qora.api.ApiErrors;
 import org.qora.api.ApiException;
 import org.qora.api.ApiExceptionFactory;
 import org.qora.api.model.SimpleTransactionSignRequest;
+import org.qora.controller.Controller;
 import org.qora.data.transaction.TransactionData;
 import org.qora.globalization.Translator;
 import org.qora.repository.DataException;
@@ -388,6 +389,9 @@ public class TransactionsResource {
 			repository.getTransactionRepository().unconfirmTransaction(transactionData);
 			repository.saveChanges();
 
+			// Notify controller of new transaction
+			Controller.getInstance().onNewTransaction(transactionData);
+
 			return "true";
 		} catch (NumberFormatException e) {
 			throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA, e);
diff --git a/src/main/java/org/qora/controller/Controller.java b/src/main/java/org/qora/controller/Controller.java
index 7e4aa7b8..7fa60b25 100644
--- a/src/main/java/org/qora/controller/Controller.java
+++ b/src/main/java/org/qora/controller/Controller.java
@@ -23,6 +23,7 @@ import org.qora.block.BlockChain;
 import org.qora.block.BlockGenerator;
 import org.qora.data.block.BlockData;
 import org.qora.data.network.PeerData;
+import org.qora.data.transaction.TransactionData;
 import org.qora.network.Network;
 import org.qora.network.Peer;
 import org.qora.network.message.BlockMessage;
@@ -31,12 +32,15 @@ import org.qora.network.message.GetSignaturesMessage;
 import org.qora.network.message.HeightMessage;
 import org.qora.network.message.Message;
 import org.qora.network.message.SignaturesMessage;
+import org.qora.network.message.TransactionMessage;
 import org.qora.repository.DataException;
 import org.qora.repository.Repository;
 import org.qora.repository.RepositoryFactory;
 import org.qora.repository.RepositoryManager;
 import org.qora.repository.hsqldb.HSQLDBRepositoryFactory;
 import org.qora.settings.Settings;
+import org.qora.transaction.Transaction;
+import org.qora.transaction.Transaction.ValidationResult;
 import org.qora.utils.Base58;
 import org.qora.utils.NTP;
 
@@ -216,26 +220,39 @@ public class Controller extends Thread {
 
 		// If we have enough peers, potentially synchronize
 		List<Peer> peers = Network.getInstance().getHandshakeCompletedPeers();
-		if (peers.size() >= Settings.getInstance().getMinPeers()) {
-			peers.removeIf(peer -> peer.getPeerData().getLastHeight() <= ourHeight);
+		if (peers.size() < Settings.getInstance().getMinPeers())
+			return;
 
-			if (!peers.isEmpty()) {
-				// Pick random peer to sync with
-				int index = new SecureRandom().nextInt(peers.size());
-				Peer peer = peers.get(index);
+		for(Peer peer : peers)
+			LOGGER.trace(String.format("Peer %s is at height %d", peer, peer.getPeerData().getLastHeight()));
 
-				if (!Synchronizer.getInstance().synchronize(peer)) {
-					// Failure so don't use this peer again for a while
-					try (final Repository repository = RepositoryManager.getRepository()) {
-						PeerData peerData = peer.getPeerData();
-						peerData.setLastMisbehaved(NTP.getTime());
-						repository.getNetworkRepository().save(peerData);
-						repository.saveChanges();
-					} catch (DataException e) {
-						LOGGER.warn("Repository issue while updating peer synchronization info", e);
-					}
+		peers.removeIf(peer -> peer.getPeerData().getLastHeight() <= ourHeight);
+
+		if (!peers.isEmpty()) {
+			// Pick random peer to sync with
+			int index = new SecureRandom().nextInt(peers.size());
+			Peer peer = peers.get(index);
+
+			if (!Synchronizer.getInstance().synchronize(peer)) {
+				LOGGER.debug(String.format("Failed to synchronize with peer %s", peer));
+
+				// Failure so don't use this peer again for a while
+				try (final Repository repository = RepositoryManager.getRepository()) {
+					PeerData peerData = peer.getPeerData();
+					peerData.setLastMisbehaved(NTP.getTime());
+					repository.getNetworkRepository().save(peerData);
+					repository.saveChanges();
+				} catch (DataException e) {
+					LOGGER.warn("Repository issue while updating peer synchronization info", e);
 				}
+
+				return;
 			}
+
+			LOGGER.debug(String.format("Synchronized with peer %s", peer));
+
+			// Broadcast our new height
+			Network.getInstance().broadcast(recipientPeer -> new HeightMessage(getChainHeight()));
 		}
 	}
 
@@ -366,9 +383,42 @@ public class Controller extends Thread {
 				}
 				break;
 
+			case TRANSACTION:
+				try (final Repository repository = RepositoryManager.getRepository()) {
+					TransactionMessage transactionMessage = (TransactionMessage) message;
+
+					TransactionData transactionData = transactionMessage.getTransactionData();
+					Transaction transaction = Transaction.fromData(repository, transactionData);
+
+					// Check signature
+					if (!transaction.isSignatureValid())
+						break;
+
+					// Do we have it already?
+					if (repository.getTransactionRepository().exists(transactionData.getSignature()))
+						break;
+
+					// Is it valid?
+					if (transaction.isValidUnconfirmed() != ValidationResult.OK)
+						break;
+
+					// Seems ok - add to unconfirmed pile
+					repository.getTransactionRepository().save(transactionData);
+					repository.getTransactionRepository().unconfirmTransaction(transactionData);
+					repository.saveChanges();
+				} catch (DataException e) {
+					LOGGER.error(String.format("Repository issue while responding to %s from peer %s", message.getType().name(), peer), e);
+				}
+				break;
+
 			default:
 				break;
 		}
 	}
 
+	public void onNewTransaction(TransactionData transactionData) {
+		// Send round to all peers
+		Network.getInstance().broadcast(peer -> new TransactionMessage(transactionData));
+	}
+
 }
diff --git a/src/main/java/org/qora/controller/Synchronizer.java b/src/main/java/org/qora/controller/Synchronizer.java
index b82c48df..7504d8c9 100644
--- a/src/main/java/org/qora/controller/Synchronizer.java
+++ b/src/main/java/org/qora/controller/Synchronizer.java
@@ -109,15 +109,13 @@ public class Synchronizer {
 							signatures.remove(0);
 							++this.ourHeight;
 
-							BlockData newBlockData = this.fetchBlockData(peer, signature);
+							Block newBlock = this.fetchBlock(repository, peer, signature);
 
-							if (newBlockData == null) {
+							if (newBlock == null) {
 								LOGGER.info(String.format("Peer %s failed to respond with block for height %d", peer, this.ourHeight));
 								return false;
 							}
 
-							Block newBlock = new Block(repository, newBlockData);
-
 							if (!newBlock.isSignatureValid()) {
 								LOGGER.info(String.format("Peer %s sent block with invalid signature for height %d", peer, this.ourHeight));
 								return false;
@@ -227,8 +225,8 @@ public class Synchronizer {
 		return blockSignatures;
 	}
 
-	private List<byte[]> getBlockSignatures(Peer peer, byte[] parentSignature, int countRequested) {
-		// TODO countRequested is v2+ feature
+	private List<byte[]> getBlockSignatures(Peer peer, byte[] parentSignature, int numberRequested) {
+		// TODO numberRequested is v2+ feature
 		Message getSignaturesMessage = new GetSignaturesMessage(parentSignature);
 
 		Message message = peer.getResponse(getSignaturesMessage);
@@ -240,7 +238,7 @@ public class Synchronizer {
 		return signaturesMessage.getSignatures();
 	}
 
-	private BlockData fetchBlockData(Peer peer, byte[] signature) {
+	private Block fetchBlock(Repository repository, Peer peer, byte[] signature) {
 		Message getBlockMessage = new GetBlockMessage(signature);
 
 		Message message = peer.getResponse(getBlockMessage);
@@ -249,7 +247,12 @@ public class Synchronizer {
 
 		BlockMessage blockMessage = (BlockMessage) message;
 
-		return blockMessage.getBlockData();
+		try {
+			return new Block(repository, blockMessage.getBlockData(), blockMessage.getTransactions(), blockMessage.getAtStates());
+		} catch (DataException e) {
+			LOGGER.debug("Failed to create block", e);
+			return null;
+		}
 	}
 
 }
diff --git a/src/main/java/org/qora/network/Handshake.java b/src/main/java/org/qora/network/Handshake.java
index 1b1368f4..ea247d73 100644
--- a/src/main/java/org/qora/network/Handshake.java
+++ b/src/main/java/org/qora/network/Handshake.java
@@ -105,7 +105,7 @@ public enum Handshake {
 		}
 	};
 
-	private static final long MAX_TIMESTAMP_DELTA = 1000; // ms
+	private static final long MAX_TIMESTAMP_DELTA = 2000; // ms
 
 	public final MessageType expectedMessageType;
 
diff --git a/src/main/java/org/qora/network/Network.java b/src/main/java/org/qora/network/Network.java
index 2372cd8c..27043fac 100644
--- a/src/main/java/org/qora/network/Network.java
+++ b/src/main/java/org/qora/network/Network.java
@@ -13,6 +13,8 @@ import java.util.Collections;
 import java.util.List;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReentrantLock;
 import java.util.function.Function;
 import java.util.function.Predicate;
 import java.util.stream.Collectors;
@@ -21,11 +23,13 @@ import org.apache.logging.log4j.LogManager;
 import org.apache.logging.log4j.Logger;
 import org.qora.controller.Controller;
 import org.qora.data.network.PeerData;
+import org.qora.data.transaction.TransactionData;
 import org.qora.network.message.HeightMessage;
 import org.qora.network.message.Message;
 import org.qora.network.message.PeersMessage;
 import org.qora.network.message.PeersV2Message;
 import org.qora.network.message.PingMessage;
+import org.qora.network.message.TransactionMessage;
 import org.qora.repository.DataException;
 import org.qora.repository.Repository;
 import org.qora.repository.RepositoryManager;
@@ -56,6 +60,7 @@ public class Network extends Thread {
 	private int maxPeers;
 	private ExecutorService peerExecutor;
 	private long nextBroadcast;
+	private Lock mergePeersLock;
 
 	// Constructors
 
@@ -92,6 +97,8 @@ public class Network extends Thread {
 
 		peerExecutor = Executors.newCachedThreadPool();
 		nextBroadcast = System.currentTimeMillis();
+
+		mergePeersLock = new ReentrantLock();
 	}
 
 	// Getters / setters
@@ -377,18 +384,35 @@ public class Network extends Thread {
 		// Make a note that we've successfully completed handshake (and when)
 		peer.getPeerData().setLastConnected(NTP.getTime());
 
+		// Start regular pings
 		peer.startPings();
 
+		// Send our height
 		Message heightMessage = new HeightMessage(Controller.getInstance().getChainHeight());
-
 		if (!peer.sendMessage(heightMessage)) {
 			peer.disconnect();
 			return;
 		}
 
+		// Send our peers list
 		Message peersMessage = this.buildPeersMessage(peer);
 		if (!peer.sendMessage(peersMessage))
 			peer.disconnect();
+
+		// Send our unconfirmed transactions
+		try (final Repository repository = RepositoryManager.getRepository()) {
+			List<TransactionData> transactions = repository.getTransactionRepository().getUnconfirmedTransactions();
+
+			for (TransactionData transactionData : transactions) {
+				Message transactionMessage = new TransactionMessage(transactionData);
+				if (!peer.sendMessage(transactionMessage)) {
+					peer.disconnect();
+					return;
+				}
+			}
+		} catch (DataException e) {
+			LOGGER.error("Repository issue while sending unconfirmed transactions", e);
+		}
 	}
 
 	/** Returns PEERS message made from peers we've connected to recently, and this node's details */
@@ -441,27 +465,42 @@ public class Network extends Thread {
 	}
 
 	private void mergePeers(List<InetSocketAddress> peerAddresses) {
-		try (final Repository repository = RepositoryManager.getRepository()) {
-			List<PeerData> knownPeers = repository.getNetworkRepository().getAllPeers();
+		mergePeersLock.lock();
 
-			// Resolve known peer hostnames
-			Function<PeerData, InetSocketAddress> peerDataToSocketAddress = peerData -> new InetSocketAddress(peerData.getSocketAddress().getHostString(),
-					peerData.getSocketAddress().getPort());
-			List<InetSocketAddress> knownPeerAddresses = knownPeers.stream().map(peerDataToSocketAddress).collect(Collectors.toList());
+		try {
+			try (final Repository repository = RepositoryManager.getRepository()) {
+				List<PeerData> knownPeers = repository.getNetworkRepository().getAllPeers();
 
-			// Filter out duplicates
-			Predicate<InetSocketAddress> addressKnown = peerAddress -> knownPeerAddresses.stream().anyMatch(knownAddress -> knownAddress.equals(peerAddress));
-			peerAddresses.removeIf(addressKnown);
+				for (PeerData peerData : knownPeers)
+					LOGGER.trace(String.format("Known peer %s", peerData.getSocketAddress()));
 
-			// Save the rest into database
-			for (InetSocketAddress peerAddress : peerAddresses) {
-				PeerData peerData = new PeerData(peerAddress);
-				repository.getNetworkRepository().save(peerData);
+				// Resolve known peer hostnames
+				Function<PeerData, InetSocketAddress> peerDataToSocketAddress = peerData -> new InetSocketAddress(peerData.getSocketAddress().getHostString(),
+						peerData.getSocketAddress().getPort());
+				List<InetSocketAddress> knownPeerAddresses = knownPeers.stream().map(peerDataToSocketAddress).collect(Collectors.toList());
+
+				for (InetSocketAddress address : knownPeerAddresses)
+					LOGGER.trace(String.format("Resolved known peer %s", address));
+
+				// Filter out duplicates
+				// We have to use our own Peer.addressEquals as InetSocketAddress.equals isn't quite right for us
+				Predicate<InetSocketAddress> addressKnown = peerAddress -> knownPeerAddresses.stream()
+						.anyMatch(knownAddress -> Peer.addressEquals(knownAddress, peerAddress));
+				peerAddresses.removeIf(addressKnown);
+
+				// Save the rest into database
+				for (InetSocketAddress peerAddress : peerAddresses) {
+					PeerData peerData = new PeerData(peerAddress);
+					LOGGER.trace(String.format("Adding new peer %s to repository", peerAddress));
+					repository.getNetworkRepository().save(peerData);
+				}
+
+				repository.saveChanges();
+			} catch (DataException e) {
+				LOGGER.error("Repository issue while merging peers list from remote node", e);
 			}
-
-			repository.saveChanges();
-		} catch (DataException e) {
-			LOGGER.error("Repository issue while merging peers list from remote node", e);
+		} finally {
+			mergePeersLock.unlock();
 		}
 	}
 
diff --git a/src/main/java/org/qora/network/Peer.java b/src/main/java/org/qora/network/Peer.java
index 32acd76c..ed62f219 100644
--- a/src/main/java/org/qora/network/Peer.java
+++ b/src/main/java/org/qora/network/Peer.java
@@ -180,6 +180,8 @@ public class Peer implements Runnable {
 				if (message == null)
 					return;
 
+				LOGGER.trace(String.format("Received %s message with ID %d from peer %s", message.getType().name(), message.getId(), this));
+
 				// Find potential blocking queue for this id (expect null if id is -1)
 				BlockingQueue<Message> queue = this.messages.get(message.getId());
 				if (queue != null) {
@@ -209,7 +211,7 @@ public class Peer implements Runnable {
 
 		try {
 			// Send message
-			LOGGER.trace(String.format("Sending %s message to peer %s", message.getType().name(), this));
+			LOGGER.trace(String.format("Sending %s message with ID %d to peer %s", message.getType().name(), message.getId(), this));
 
 			synchronized (this.out) {
 				this.out.write(message.toBytes());
@@ -309,4 +311,12 @@ public class Peer implements Runnable {
 		Network.getInstance().onDisconnect(this);
 	}
 
+	/** Returns true if ports and addresses (or hostnames) match */
+	public static boolean addressEquals(InetSocketAddress knownAddress, InetSocketAddress peerAddress) {
+		if (knownAddress.getPort() != peerAddress.getPort())
+			return false;
+
+		return knownAddress.getHostString().equalsIgnoreCase(peerAddress.getHostString());
+	}
+
 }
diff --git a/src/main/java/org/qora/network/Proof.java b/src/main/java/org/qora/network/Proof.java
index d32e7f05..12e05801 100644
--- a/src/main/java/org/qora/network/Proof.java
+++ b/src/main/java/org/qora/network/Proof.java
@@ -12,16 +12,6 @@ import com.google.common.primitives.Longs;
 public class Proof extends Thread {
 
 	private static final int MIN_PROOF_ZEROS = 2;
-	private static final MessageDigest sha256;
-	static {
-		try {
-			sha256 = MessageDigest.getInstance("SHA256");
-		} catch (NoSuchAlgorithmException e) {
-			// Can't progress
-			throw new RuntimeException("Message digest SHA256 not available");
-		}
-	}
-
 	private static final HashSet<Long> seenSalts = new HashSet<>();
 
 	private Peer peer;
@@ -63,6 +53,14 @@ public class Proof extends Thread {
 		byte[] timestampBytes = Longs.toByteArray(timestamp);
 		System.arraycopy(timestampBytes, 0, message, 8 + 8, timestampBytes.length);
 
+		MessageDigest sha256;
+		try {
+			sha256 = MessageDigest.getInstance("SHA256");
+		} catch (NoSuchAlgorithmException e) {
+			// Can't progress
+			throw new RuntimeException("Message digest SHA256 not available");
+		}
+
 		long nonce;
 		for (nonce = 0; nonce < Long.MAX_VALUE; ++nonce) {
 			// Check whether we're shutting down every so often
@@ -77,6 +75,8 @@ public class Proof extends Thread {
 
 			if (check(digest))
 				break;
+
+			sha256.reset();
 		}
 
 		ProofMessage proofMessage = new ProofMessage(timestamp, salt, nonce);
@@ -104,6 +104,14 @@ public class Proof extends Thread {
 		byte[] nonceBytes = Longs.toByteArray(nonce);
 		System.arraycopy(nonceBytes, 0, message, 0, nonceBytes.length);
 
+		MessageDigest sha256;
+		try {
+			sha256 = MessageDigest.getInstance("SHA256");
+		} catch (NoSuchAlgorithmException e) {
+			// Can't progress
+			throw new RuntimeException("Message digest SHA256 not available");
+		}
+
 		byte[] digest = sha256.digest(message);
 
 		return check(digest);
diff --git a/src/main/java/org/qora/network/message/TransactionMessage.java b/src/main/java/org/qora/network/message/TransactionMessage.java
new file mode 100644
index 00000000..a492f6d2
--- /dev/null
+++ b/src/main/java/org/qora/network/message/TransactionMessage.java
@@ -0,0 +1,50 @@
+package org.qora.network.message;
+
+import java.io.UnsupportedEncodingException;
+import java.nio.ByteBuffer;
+
+import org.qora.data.transaction.TransactionData;
+import org.qora.transform.TransformationException;
+import org.qora.transform.transaction.TransactionTransformer;
+
+public class TransactionMessage extends Message {
+
+	private TransactionData transactionData;
+
+	public TransactionMessage(TransactionData transactionData) {
+		this(-1, transactionData);
+	}
+
+	private TransactionMessage(int id, TransactionData transactionData) {
+		super(id, MessageType.TRANSACTION);
+
+		this.transactionData = transactionData;
+	}
+
+	public TransactionData getTransactionData() {
+		return this.transactionData;
+	}
+
+	public static Message fromByteBuffer(int id, ByteBuffer byteBuffer) throws UnsupportedEncodingException {
+		try {
+			TransactionData transactionData = TransactionTransformer.fromByteBuffer(byteBuffer);
+
+			return new TransactionMessage(id, transactionData);
+		} catch (TransformationException e) {
+			return null;
+		}
+	}
+
+	@Override
+	protected byte[] toData() {
+		if (this.transactionData == null)
+			return null;
+
+		try {
+			return TransactionTransformer.toBytes(this.transactionData);
+		} catch (TransformationException e) {
+			return null;
+		}
+	}
+
+}
diff --git a/src/main/java/org/qora/repository/TransactionRepository.java b/src/main/java/org/qora/repository/TransactionRepository.java
index 97a6aa34..026340ac 100644
--- a/src/main/java/org/qora/repository/TransactionRepository.java
+++ b/src/main/java/org/qora/repository/TransactionRepository.java
@@ -19,6 +19,8 @@ public interface TransactionRepository {
 	/** Returns block height containing transaction or 0 if not in a block or transaction doesn't exist */
 	public int getHeightFromSignature(byte[] signature) throws DataException;
 
+	public boolean exists(byte[] signature) throws DataException;
+
 	// Transaction participants
 
 	public List<byte[]> getSignaturesInvolvingAddress(String address) throws DataException;
diff --git a/src/main/java/org/qora/repository/hsqldb/HSQLDBDatabaseUpdates.java b/src/main/java/org/qora/repository/hsqldb/HSQLDBDatabaseUpdates.java
index e41e2c31..33857895 100644
--- a/src/main/java/org/qora/repository/hsqldb/HSQLDBDatabaseUpdates.java
+++ b/src/main/java/org/qora/repository/hsqldb/HSQLDBDatabaseUpdates.java
@@ -77,6 +77,7 @@ public class HSQLDBDatabaseUpdates {
 					stmt.execute("SET DATABASE SQL NAMES TRUE"); // SQL keywords cannot be used as DB object names, e.g. table names
 					stmt.execute("SET DATABASE SQL SYNTAX MYS TRUE"); // Required for our use of INSERT ... ON DUPLICATE KEY UPDATE ... syntax
 					stmt.execute("SET DATABASE SQL RESTRICT EXEC TRUE"); // No multiple-statement execute() or DDL/DML executeQuery()
+					stmt.execute("SET DATABASE TRANSACTION CONTROL MVCC"); // Use MVCC over default two-phase locking, a-k-a "LOCKS"
 					stmt.execute("SET DATABASE DEFAULT TABLE TYPE CACHED");
 					stmt.execute("SET DATABASE COLLATION SQL_TEXT NO PAD"); // Do not pad strings to same length before comparison
 					stmt.execute("CREATE COLLATION SQL_TEXT_UCC_NO_PAD FOR SQL_TEXT FROM SQL_TEXT_UCC NO PAD");
diff --git a/src/main/java/org/qora/repository/hsqldb/HSQLDBRepository.java b/src/main/java/org/qora/repository/hsqldb/HSQLDBRepository.java
index 09481e09..efc6e1e1 100644
--- a/src/main/java/org/qora/repository/hsqldb/HSQLDBRepository.java
+++ b/src/main/java/org/qora/repository/hsqldb/HSQLDBRepository.java
@@ -357,4 +357,33 @@ public class HSQLDBRepository implements Repository {
 		return sql;
 	}
 
-}
+	/** Logs other HSQLDB sessions then re-throws passed exception */
+	public SQLException examineException(SQLException e) throws SQLException {
+		LOGGER.error("SQL error: " + e.getMessage());
+
+		// Serialization failure / potential deadlock - so list other sessions
+		try (ResultSet resultSet = this.checkedExecute(
+				"SELECT session_id, transaction, transaction_size, waiting_for_this, this_waiting_for, current_statement FROM Information_schema.system_sessions")) {
+			if (resultSet == null)
+				return e;
+
+			do {
+				long sessionId = resultSet.getLong(1);
+				boolean inTransaction = resultSet.getBoolean(2);
+				long transactionSize = resultSet.getLong(3);
+				String waitingForThis = resultSet.getString(4);
+				String thisWaitingFor = resultSet.getString(5);
+				String currentStatement = resultSet.getString(6);
+
+				LOGGER.error(String.format("Session %d, %s transaction (size %d), waiting for this '%s', this waiting for '%s', current statement: %s",
+						sessionId, (inTransaction ? "in" : "not in"), transactionSize, waitingForThis, thisWaitingFor, currentStatement));
+			} while (resultSet.next());
+		} catch (SQLException de) {
+			// Throw original exception instead
+			return e;
+		}
+
+		return e;
+	}
+
+}
\ No newline at end of file
diff --git a/src/main/java/org/qora/repository/hsqldb/HSQLDBSaver.java b/src/main/java/org/qora/repository/hsqldb/HSQLDBSaver.java
index f34d5dc4..b004b647 100644
--- a/src/main/java/org/qora/repository/hsqldb/HSQLDBSaver.java
+++ b/src/main/java/org/qora/repository/hsqldb/HSQLDBSaver.java
@@ -62,6 +62,8 @@ public class HSQLDBSaver {
 			this.bindValues(preparedStatement);
 
 			return preparedStatement.execute();
+		} catch (SQLException e) {
+			throw repository.examineException(e);
 		}
 	}
 
diff --git a/src/main/java/org/qora/repository/hsqldb/transaction/HSQLDBTransactionRepository.java b/src/main/java/org/qora/repository/hsqldb/transaction/HSQLDBTransactionRepository.java
index fb5054cf..e61ea4e2 100644
--- a/src/main/java/org/qora/repository/hsqldb/transaction/HSQLDBTransactionRepository.java
+++ b/src/main/java/org/qora/repository/hsqldb/transaction/HSQLDBTransactionRepository.java
@@ -251,6 +251,15 @@ public class HSQLDBTransactionRepository implements TransactionRepository {
 		}
 	}
 
+	@Override
+	public boolean exists(byte[] signature) throws DataException {
+		try {
+			return this.repository.exists("Transactions", "signature = ?", signature);
+		} catch (SQLException e) {
+			throw new DataException("Unable to check for transaction in repository", e);
+		}
+	}
+
 	@Override
 	public List<byte[]> getSignaturesInvolvingAddress(String address) throws DataException {
 		List<byte[]> signatures = new ArrayList<byte[]>();
diff --git a/src/main/java/org/qora/transform/transaction/AddGroupAdminTransactionTransformer.java b/src/main/java/org/qora/transform/transaction/AddGroupAdminTransactionTransformer.java
index 7f07f48d..69460da3 100644
--- a/src/main/java/org/qora/transform/transaction/AddGroupAdminTransactionTransformer.java
+++ b/src/main/java/org/qora/transform/transaction/AddGroupAdminTransactionTransformer.java
@@ -40,7 +40,7 @@ public class AddGroupAdminTransactionTransformer extends TransactionTransformer
 		layout.add("signature", TransformationType.SIGNATURE);
 	}
 
-	static TransactionData fromByteBuffer(ByteBuffer byteBuffer) throws TransformationException {
+	public static TransactionData fromByteBuffer(ByteBuffer byteBuffer) throws TransformationException {
 		long timestamp = byteBuffer.getLong();
 
 		byte[] reference = new byte[REFERENCE_LENGTH];
diff --git a/src/main/java/org/qora/transform/transaction/ArbitraryTransactionTransformer.java b/src/main/java/org/qora/transform/transaction/ArbitraryTransactionTransformer.java
index fb592c8f..e592cb6d 100644
--- a/src/main/java/org/qora/transform/transaction/ArbitraryTransactionTransformer.java
+++ b/src/main/java/org/qora/transform/transaction/ArbitraryTransactionTransformer.java
@@ -59,7 +59,7 @@ public class ArbitraryTransactionTransformer extends TransactionTransformer {
 		layout.add("signature", TransformationType.SIGNATURE);
 	}
 
-	static TransactionData fromByteBuffer(ByteBuffer byteBuffer) throws TransformationException {
+	public static TransactionData fromByteBuffer(ByteBuffer byteBuffer) throws TransformationException {
 		long timestamp = byteBuffer.getLong();
 
 		int version = ArbitraryTransaction.getVersionByTimestamp(timestamp);
diff --git a/src/main/java/org/qora/transform/transaction/AtTransactionTransformer.java b/src/main/java/org/qora/transform/transaction/AtTransactionTransformer.java
index bd399c1c..3500f670 100644
--- a/src/main/java/org/qora/transform/transaction/AtTransactionTransformer.java
+++ b/src/main/java/org/qora/transform/transaction/AtTransactionTransformer.java
@@ -26,7 +26,7 @@ public class AtTransactionTransformer extends TransactionTransformer {
 	private static final int TYPELESS_DATALESS_LENGTH = BASE_TYPELESS_LENGTH + SENDER_LENGTH + RECIPIENT_LENGTH + AMOUNT_LENGTH + ASSET_ID_LENGTH
 			+ DATA_SIZE_LENGTH;
 
-	static TransactionData fromByteBuffer(ByteBuffer byteBuffer) throws TransformationException {
+	public static TransactionData fromByteBuffer(ByteBuffer byteBuffer) throws TransformationException {
 		throw new TransformationException("Serialized AT Transactions should not exist!");
 	}
 
diff --git a/src/main/java/org/qora/transform/transaction/BuyNameTransactionTransformer.java b/src/main/java/org/qora/transform/transaction/BuyNameTransactionTransformer.java
index dbbe144a..c27b6c50 100644
--- a/src/main/java/org/qora/transform/transaction/BuyNameTransactionTransformer.java
+++ b/src/main/java/org/qora/transform/transaction/BuyNameTransactionTransformer.java
@@ -45,7 +45,7 @@ public class BuyNameTransactionTransformer extends TransactionTransformer {
 		layout.add("signature", TransformationType.SIGNATURE);
 	}
 
-	static TransactionData fromByteBuffer(ByteBuffer byteBuffer) throws TransformationException {
+	public static TransactionData fromByteBuffer(ByteBuffer byteBuffer) throws TransformationException {
 		long timestamp = byteBuffer.getLong();
 
 		byte[] reference = new byte[REFERENCE_LENGTH];
diff --git a/src/main/java/org/qora/transform/transaction/CancelAssetOrderTransactionTransformer.java b/src/main/java/org/qora/transform/transaction/CancelAssetOrderTransactionTransformer.java
index 7845c095..0bdab13d 100644
--- a/src/main/java/org/qora/transform/transaction/CancelAssetOrderTransactionTransformer.java
+++ b/src/main/java/org/qora/transform/transaction/CancelAssetOrderTransactionTransformer.java
@@ -39,7 +39,7 @@ public class CancelAssetOrderTransactionTransformer extends TransactionTransform
 		layout.add("signature", TransformationType.SIGNATURE);
 	}
 
-	static TransactionData fromByteBuffer(ByteBuffer byteBuffer) throws TransformationException {
+	public static TransactionData fromByteBuffer(ByteBuffer byteBuffer) throws TransformationException {
 		long timestamp = byteBuffer.getLong();
 
 		byte[] reference = new byte[REFERENCE_LENGTH];
diff --git a/src/main/java/org/qora/transform/transaction/CancelGroupBanTransactionTransformer.java b/src/main/java/org/qora/transform/transaction/CancelGroupBanTransactionTransformer.java
index 2a2f3fe2..07ec79f1 100644
--- a/src/main/java/org/qora/transform/transaction/CancelGroupBanTransactionTransformer.java
+++ b/src/main/java/org/qora/transform/transaction/CancelGroupBanTransactionTransformer.java
@@ -40,7 +40,7 @@ public class CancelGroupBanTransactionTransformer extends TransactionTransformer
 		layout.add("signature", TransformationType.SIGNATURE);
 	}
 
-	static TransactionData fromByteBuffer(ByteBuffer byteBuffer) throws TransformationException {
+	public static TransactionData fromByteBuffer(ByteBuffer byteBuffer) throws TransformationException {
 		long timestamp = byteBuffer.getLong();
 
 		byte[] reference = new byte[REFERENCE_LENGTH];
diff --git a/src/main/java/org/qora/transform/transaction/CancelGroupInviteTransactionTransformer.java b/src/main/java/org/qora/transform/transaction/CancelGroupInviteTransactionTransformer.java
index c79a2e26..1a0f6a77 100644
--- a/src/main/java/org/qora/transform/transaction/CancelGroupInviteTransactionTransformer.java
+++ b/src/main/java/org/qora/transform/transaction/CancelGroupInviteTransactionTransformer.java
@@ -40,7 +40,7 @@ public class CancelGroupInviteTransactionTransformer extends TransactionTransfor
 		layout.add("signature", TransformationType.SIGNATURE);
 	}
 
-	static TransactionData fromByteBuffer(ByteBuffer byteBuffer) throws TransformationException {
+	public static TransactionData fromByteBuffer(ByteBuffer byteBuffer) throws TransformationException {
 		long timestamp = byteBuffer.getLong();
 
 		byte[] reference = new byte[REFERENCE_LENGTH];
diff --git a/src/main/java/org/qora/transform/transaction/CancelSellNameTransactionTransformer.java b/src/main/java/org/qora/transform/transaction/CancelSellNameTransactionTransformer.java
index ba27b53c..5e4f1d83 100644
--- a/src/main/java/org/qora/transform/transaction/CancelSellNameTransactionTransformer.java
+++ b/src/main/java/org/qora/transform/transaction/CancelSellNameTransactionTransformer.java
@@ -41,7 +41,7 @@ public class CancelSellNameTransactionTransformer extends TransactionTransformer
 		layout.add("signature", TransformationType.SIGNATURE);
 	}
 
-	static TransactionData fromByteBuffer(ByteBuffer byteBuffer) throws TransformationException {
+	public static TransactionData fromByteBuffer(ByteBuffer byteBuffer) throws TransformationException {
 		long timestamp = byteBuffer.getLong();
 
 		byte[] reference = new byte[REFERENCE_LENGTH];
diff --git a/src/main/java/org/qora/transform/transaction/CreateAssetOrderTransactionTransformer.java b/src/main/java/org/qora/transform/transaction/CreateAssetOrderTransactionTransformer.java
index ce989202..bfbb2d28 100644
--- a/src/main/java/org/qora/transform/transaction/CreateAssetOrderTransactionTransformer.java
+++ b/src/main/java/org/qora/transform/transaction/CreateAssetOrderTransactionTransformer.java
@@ -43,7 +43,7 @@ public class CreateAssetOrderTransactionTransformer extends TransactionTransform
 		layout.add("signature", TransformationType.SIGNATURE);
 	}
 
-	static TransactionData fromByteBuffer(ByteBuffer byteBuffer) throws TransformationException {
+	public static TransactionData fromByteBuffer(ByteBuffer byteBuffer) throws TransformationException {
 		long timestamp = byteBuffer.getLong();
 
 		byte[] reference = new byte[REFERENCE_LENGTH];
diff --git a/src/main/java/org/qora/transform/transaction/CreateGroupTransactionTransformer.java b/src/main/java/org/qora/transform/transaction/CreateGroupTransactionTransformer.java
index 069260ab..122e8c74 100644
--- a/src/main/java/org/qora/transform/transaction/CreateGroupTransactionTransformer.java
+++ b/src/main/java/org/qora/transform/transaction/CreateGroupTransactionTransformer.java
@@ -48,7 +48,7 @@ public class CreateGroupTransactionTransformer extends TransactionTransformer {
 		layout.add("signature", TransformationType.SIGNATURE);
 	}
 
-	static TransactionData fromByteBuffer(ByteBuffer byteBuffer) throws TransformationException {
+	public static TransactionData fromByteBuffer(ByteBuffer byteBuffer) throws TransformationException {
 		long timestamp = byteBuffer.getLong();
 
 		byte[] reference = new byte[REFERENCE_LENGTH];
diff --git a/src/main/java/org/qora/transform/transaction/CreatePollTransactionTransformer.java b/src/main/java/org/qora/transform/transaction/CreatePollTransactionTransformer.java
index 9079188c..ee7b6aa0 100644
--- a/src/main/java/org/qora/transform/transaction/CreatePollTransactionTransformer.java
+++ b/src/main/java/org/qora/transform/transaction/CreatePollTransactionTransformer.java
@@ -55,7 +55,7 @@ public class CreatePollTransactionTransformer extends TransactionTransformer {
 		layout.add("signature", TransformationType.SIGNATURE);
 	}
 
-	static TransactionData fromByteBuffer(ByteBuffer byteBuffer) throws TransformationException {
+	public static TransactionData fromByteBuffer(ByteBuffer byteBuffer) throws TransformationException {
 		long timestamp = byteBuffer.getLong();
 
 		byte[] reference = new byte[REFERENCE_LENGTH];
diff --git a/src/main/java/org/qora/transform/transaction/DeployAtTransactionTransformer.java b/src/main/java/org/qora/transform/transaction/DeployAtTransactionTransformer.java
index f354735c..9317d8e2 100644
--- a/src/main/java/org/qora/transform/transaction/DeployAtTransactionTransformer.java
+++ b/src/main/java/org/qora/transform/transaction/DeployAtTransactionTransformer.java
@@ -59,7 +59,7 @@ public class DeployAtTransactionTransformer extends TransactionTransformer {
 		layout.add("signature", TransformationType.SIGNATURE);
 	}
 
-	static TransactionData fromByteBuffer(ByteBuffer byteBuffer) throws TransformationException {
+	public static TransactionData fromByteBuffer(ByteBuffer byteBuffer) throws TransformationException {
 		long timestamp = byteBuffer.getLong();
 
 		int version = DeployAtTransaction.getVersionByTimestamp(timestamp);
diff --git a/src/main/java/org/qora/transform/transaction/GenesisTransactionTransformer.java b/src/main/java/org/qora/transform/transaction/GenesisTransactionTransformer.java
index c317341c..0448046b 100644
--- a/src/main/java/org/qora/transform/transaction/GenesisTransactionTransformer.java
+++ b/src/main/java/org/qora/transform/transaction/GenesisTransactionTransformer.java
@@ -41,7 +41,7 @@ public class GenesisTransactionTransformer extends TransactionTransformer {
 		layout.add("signature", TransformationType.SIGNATURE);
 	}
 
-	static TransactionData fromByteBuffer(ByteBuffer byteBuffer) throws TransformationException {
+	public static TransactionData fromByteBuffer(ByteBuffer byteBuffer) throws TransformationException {
 		long timestamp = byteBuffer.getLong();
 
 		String recipient = Serialization.deserializeAddress(byteBuffer);
diff --git a/src/main/java/org/qora/transform/transaction/GroupBanTransactionTransformer.java b/src/main/java/org/qora/transform/transaction/GroupBanTransactionTransformer.java
index 448ee4bb..2e0a0c7e 100644
--- a/src/main/java/org/qora/transform/transaction/GroupBanTransactionTransformer.java
+++ b/src/main/java/org/qora/transform/transaction/GroupBanTransactionTransformer.java
@@ -47,7 +47,7 @@ public class GroupBanTransactionTransformer extends TransactionTransformer {
 		layout.add("signature", TransformationType.SIGNATURE);
 	}
 
-	static TransactionData fromByteBuffer(ByteBuffer byteBuffer) throws TransformationException {
+	public static TransactionData fromByteBuffer(ByteBuffer byteBuffer) throws TransformationException {
 		long timestamp = byteBuffer.getLong();
 
 		byte[] reference = new byte[REFERENCE_LENGTH];
diff --git a/src/main/java/org/qora/transform/transaction/GroupInviteTransactionTransformer.java b/src/main/java/org/qora/transform/transaction/GroupInviteTransactionTransformer.java
index b46d2231..98c2df43 100644
--- a/src/main/java/org/qora/transform/transaction/GroupInviteTransactionTransformer.java
+++ b/src/main/java/org/qora/transform/transaction/GroupInviteTransactionTransformer.java
@@ -42,7 +42,7 @@ public class GroupInviteTransactionTransformer extends TransactionTransformer {
 		layout.add("signature", TransformationType.SIGNATURE);
 	}
 
-	static TransactionData fromByteBuffer(ByteBuffer byteBuffer) throws TransformationException {
+	public static TransactionData fromByteBuffer(ByteBuffer byteBuffer) throws TransformationException {
 		long timestamp = byteBuffer.getLong();
 
 		byte[] reference = new byte[REFERENCE_LENGTH];
diff --git a/src/main/java/org/qora/transform/transaction/GroupKickTransactionTransformer.java b/src/main/java/org/qora/transform/transaction/GroupKickTransactionTransformer.java
index a98807f6..a2dd9bfe 100644
--- a/src/main/java/org/qora/transform/transaction/GroupKickTransactionTransformer.java
+++ b/src/main/java/org/qora/transform/transaction/GroupKickTransactionTransformer.java
@@ -45,7 +45,7 @@ public class GroupKickTransactionTransformer extends TransactionTransformer {
 		layout.add("signature", TransformationType.SIGNATURE);
 	}
 
-	static TransactionData fromByteBuffer(ByteBuffer byteBuffer) throws TransformationException {
+	public static TransactionData fromByteBuffer(ByteBuffer byteBuffer) throws TransformationException {
 		long timestamp = byteBuffer.getLong();
 
 		byte[] reference = new byte[REFERENCE_LENGTH];
diff --git a/src/main/java/org/qora/transform/transaction/IssueAssetTransactionTransformer.java b/src/main/java/org/qora/transform/transaction/IssueAssetTransactionTransformer.java
index 65fce444..6da7819f 100644
--- a/src/main/java/org/qora/transform/transaction/IssueAssetTransactionTransformer.java
+++ b/src/main/java/org/qora/transform/transaction/IssueAssetTransactionTransformer.java
@@ -54,7 +54,7 @@ public class IssueAssetTransactionTransformer extends TransactionTransformer {
 		layout.add("signature", TransformationType.SIGNATURE);
 	}
 
-	static TransactionData fromByteBuffer(ByteBuffer byteBuffer) throws TransformationException {
+	public static TransactionData fromByteBuffer(ByteBuffer byteBuffer) throws TransformationException {
 		long timestamp = byteBuffer.getLong();
 
 		byte[] reference = new byte[REFERENCE_LENGTH];
diff --git a/src/main/java/org/qora/transform/transaction/JoinGroupTransactionTransformer.java b/src/main/java/org/qora/transform/transaction/JoinGroupTransactionTransformer.java
index 317d4c2a..26bab418 100644
--- a/src/main/java/org/qora/transform/transaction/JoinGroupTransactionTransformer.java
+++ b/src/main/java/org/qora/transform/transaction/JoinGroupTransactionTransformer.java
@@ -38,7 +38,7 @@ public class JoinGroupTransactionTransformer extends TransactionTransformer {
 		layout.add("signature", TransformationType.SIGNATURE);
 	}
 
-	static TransactionData fromByteBuffer(ByteBuffer byteBuffer) throws TransformationException {
+	public static TransactionData fromByteBuffer(ByteBuffer byteBuffer) throws TransformationException {
 		long timestamp = byteBuffer.getLong();
 
 		byte[] reference = new byte[REFERENCE_LENGTH];
diff --git a/src/main/java/org/qora/transform/transaction/LeaveGroupTransactionTransformer.java b/src/main/java/org/qora/transform/transaction/LeaveGroupTransactionTransformer.java
index a46bfb02..77c4634b 100644
--- a/src/main/java/org/qora/transform/transaction/LeaveGroupTransactionTransformer.java
+++ b/src/main/java/org/qora/transform/transaction/LeaveGroupTransactionTransformer.java
@@ -38,7 +38,7 @@ public class LeaveGroupTransactionTransformer extends TransactionTransformer {
 		layout.add("signature", TransformationType.SIGNATURE);
 	}
 
-	static TransactionData fromByteBuffer(ByteBuffer byteBuffer) throws TransformationException {
+	public static TransactionData fromByteBuffer(ByteBuffer byteBuffer) throws TransformationException {
 		long timestamp = byteBuffer.getLong();
 
 		byte[] reference = new byte[REFERENCE_LENGTH];
diff --git a/src/main/java/org/qora/transform/transaction/MessageTransactionTransformer.java b/src/main/java/org/qora/transform/transaction/MessageTransactionTransformer.java
index 57ce16dc..7f6d06ca 100644
--- a/src/main/java/org/qora/transform/transaction/MessageTransactionTransformer.java
+++ b/src/main/java/org/qora/transform/transaction/MessageTransactionTransformer.java
@@ -55,7 +55,7 @@ public class MessageTransactionTransformer extends TransactionTransformer {
 		layout.add("signature", TransformationType.SIGNATURE);
 	}
 
-	static TransactionData fromByteBuffer(ByteBuffer byteBuffer) throws TransformationException {
+	public static TransactionData fromByteBuffer(ByteBuffer byteBuffer) throws TransformationException {
 		long timestamp = byteBuffer.getLong();
 
 		int version = MessageTransaction.getVersionByTimestamp(timestamp);
diff --git a/src/main/java/org/qora/transform/transaction/MultiPaymentTransactionTransformer.java b/src/main/java/org/qora/transform/transaction/MultiPaymentTransactionTransformer.java
index c5c14747..3b6c29bb 100644
--- a/src/main/java/org/qora/transform/transaction/MultiPaymentTransactionTransformer.java
+++ b/src/main/java/org/qora/transform/transaction/MultiPaymentTransactionTransformer.java
@@ -48,7 +48,7 @@ public class MultiPaymentTransactionTransformer extends TransactionTransformer {
 		layout.add("signature", TransformationType.SIGNATURE);
 	}
 
-	static TransactionData fromByteBuffer(ByteBuffer byteBuffer) throws TransformationException {
+	public static TransactionData fromByteBuffer(ByteBuffer byteBuffer) throws TransformationException {
 		long timestamp = byteBuffer.getLong();
 
 		byte[] reference = new byte[REFERENCE_LENGTH];
diff --git a/src/main/java/org/qora/transform/transaction/PaymentTransactionTransformer.java b/src/main/java/org/qora/transform/transaction/PaymentTransactionTransformer.java
index bf7e45fc..636ad910 100644
--- a/src/main/java/org/qora/transform/transaction/PaymentTransactionTransformer.java
+++ b/src/main/java/org/qora/transform/transaction/PaymentTransactionTransformer.java
@@ -40,7 +40,7 @@ public class PaymentTransactionTransformer extends TransactionTransformer {
 		layout.add("signature", TransformationType.SIGNATURE);
 	}
 
-	static TransactionData fromByteBuffer(ByteBuffer byteBuffer) throws TransformationException {
+	public static TransactionData fromByteBuffer(ByteBuffer byteBuffer) throws TransformationException {
 		long timestamp = byteBuffer.getLong();
 
 		byte[] reference = new byte[REFERENCE_LENGTH];
diff --git a/src/main/java/org/qora/transform/transaction/RegisterNameTransactionTransformer.java b/src/main/java/org/qora/transform/transaction/RegisterNameTransactionTransformer.java
index 6a5bb1b3..9cdf4c91 100644
--- a/src/main/java/org/qora/transform/transaction/RegisterNameTransactionTransformer.java
+++ b/src/main/java/org/qora/transform/transaction/RegisterNameTransactionTransformer.java
@@ -46,7 +46,7 @@ public class RegisterNameTransactionTransformer extends TransactionTransformer {
 		layout.add("signature", TransformationType.SIGNATURE);
 	}
 
-	static TransactionData fromByteBuffer(ByteBuffer byteBuffer) throws TransformationException {
+	public static TransactionData fromByteBuffer(ByteBuffer byteBuffer) throws TransformationException {
 		long timestamp = byteBuffer.getLong();
 
 		byte[] reference = new byte[REFERENCE_LENGTH];
diff --git a/src/main/java/org/qora/transform/transaction/RemoveGroupAdminTransactionTransformer.java b/src/main/java/org/qora/transform/transaction/RemoveGroupAdminTransactionTransformer.java
index 6adbca0e..2d265b89 100644
--- a/src/main/java/org/qora/transform/transaction/RemoveGroupAdminTransactionTransformer.java
+++ b/src/main/java/org/qora/transform/transaction/RemoveGroupAdminTransactionTransformer.java
@@ -40,7 +40,7 @@ public class RemoveGroupAdminTransactionTransformer extends TransactionTransform
 		layout.add("signature", TransformationType.SIGNATURE);
 	}
 
-	static TransactionData fromByteBuffer(ByteBuffer byteBuffer) throws TransformationException {
+	public static TransactionData fromByteBuffer(ByteBuffer byteBuffer) throws TransformationException {
 		long timestamp = byteBuffer.getLong();
 
 		byte[] reference = new byte[REFERENCE_LENGTH];
diff --git a/src/main/java/org/qora/transform/transaction/SellNameTransactionTransformer.java b/src/main/java/org/qora/transform/transaction/SellNameTransactionTransformer.java
index 2b402df3..66af7b53 100644
--- a/src/main/java/org/qora/transform/transaction/SellNameTransactionTransformer.java
+++ b/src/main/java/org/qora/transform/transaction/SellNameTransactionTransformer.java
@@ -43,7 +43,7 @@ public class SellNameTransactionTransformer extends TransactionTransformer {
 		layout.add("signature", TransformationType.SIGNATURE);
 	}
 
-	static TransactionData fromByteBuffer(ByteBuffer byteBuffer) throws TransformationException {
+	public static TransactionData fromByteBuffer(ByteBuffer byteBuffer) throws TransformationException {
 		long timestamp = byteBuffer.getLong();
 
 		byte[] reference = new byte[REFERENCE_LENGTH];
diff --git a/src/main/java/org/qora/transform/transaction/TransactionTransformer.java b/src/main/java/org/qora/transform/transaction/TransactionTransformer.java
index edbbdca4..8d079762 100644
--- a/src/main/java/org/qora/transform/transaction/TransactionTransformer.java
+++ b/src/main/java/org/qora/transform/transaction/TransactionTransformer.java
@@ -190,13 +190,17 @@ public abstract class TransactionTransformer extends Transformer {
 		if (bytes == null)
 			return null;
 
-		if (bytes.length < TYPE_LENGTH)
-			throw new TransformationException("Byte data too short to determine transaction type");
-
 		LOGGER.trace("tx hex: " + HashCode.fromBytes(bytes).toString());
 
 		ByteBuffer byteBuffer = ByteBuffer.wrap(bytes);
 
+		return fromByteBuffer(byteBuffer);
+	}
+
+	public static TransactionData fromByteBuffer(ByteBuffer byteBuffer) throws TransformationException {
+		if (byteBuffer.remaining() < TYPE_LENGTH)
+			throw new TransformationException("Byte data too short to determine transaction type");
+
 		TransactionType type = TransactionType.valueOf(byteBuffer.getInt());
 		if (type == null)
 			return null;
diff --git a/src/main/java/org/qora/transform/transaction/TransferAssetTransactionTransformer.java b/src/main/java/org/qora/transform/transaction/TransferAssetTransactionTransformer.java
index 5d89d8eb..d6b0d650 100644
--- a/src/main/java/org/qora/transform/transaction/TransferAssetTransactionTransformer.java
+++ b/src/main/java/org/qora/transform/transaction/TransferAssetTransactionTransformer.java
@@ -41,7 +41,7 @@ public class TransferAssetTransactionTransformer extends TransactionTransformer
 		layout.add("signature", TransformationType.SIGNATURE);
 	}
 
-	static TransactionData fromByteBuffer(ByteBuffer byteBuffer) throws TransformationException {
+	public static TransactionData fromByteBuffer(ByteBuffer byteBuffer) throws TransformationException {
 		long timestamp = byteBuffer.getLong();
 
 		byte[] reference = new byte[REFERENCE_LENGTH];
diff --git a/src/main/java/org/qora/transform/transaction/UpdateGroupTransactionTransformer.java b/src/main/java/org/qora/transform/transaction/UpdateGroupTransactionTransformer.java
index abda115e..5d63f331 100644
--- a/src/main/java/org/qora/transform/transaction/UpdateGroupTransactionTransformer.java
+++ b/src/main/java/org/qora/transform/transaction/UpdateGroupTransactionTransformer.java
@@ -48,7 +48,7 @@ public class UpdateGroupTransactionTransformer extends TransactionTransformer {
 		layout.add("signature", TransformationType.SIGNATURE);
 	}
 
-	static TransactionData fromByteBuffer(ByteBuffer byteBuffer) throws TransformationException {
+	public static TransactionData fromByteBuffer(ByteBuffer byteBuffer) throws TransformationException {
 		long timestamp = byteBuffer.getLong();
 
 		byte[] reference = new byte[REFERENCE_LENGTH];
diff --git a/src/main/java/org/qora/transform/transaction/UpdateNameTransactionTransformer.java b/src/main/java/org/qora/transform/transaction/UpdateNameTransactionTransformer.java
index 0bc47b8d..58beec5e 100644
--- a/src/main/java/org/qora/transform/transaction/UpdateNameTransactionTransformer.java
+++ b/src/main/java/org/qora/transform/transaction/UpdateNameTransactionTransformer.java
@@ -46,7 +46,7 @@ public class UpdateNameTransactionTransformer extends TransactionTransformer {
 		layout.add("signature", TransformationType.SIGNATURE);
 	}
 
-	static TransactionData fromByteBuffer(ByteBuffer byteBuffer) throws TransformationException {
+	public static TransactionData fromByteBuffer(ByteBuffer byteBuffer) throws TransformationException {
 		long timestamp = byteBuffer.getLong();
 
 		byte[] reference = new byte[REFERENCE_LENGTH];
diff --git a/src/main/java/org/qora/transform/transaction/VoteOnPollTransactionTransformer.java b/src/main/java/org/qora/transform/transaction/VoteOnPollTransactionTransformer.java
index 82e72151..171f46a7 100644
--- a/src/main/java/org/qora/transform/transaction/VoteOnPollTransactionTransformer.java
+++ b/src/main/java/org/qora/transform/transaction/VoteOnPollTransactionTransformer.java
@@ -42,7 +42,7 @@ public class VoteOnPollTransactionTransformer extends TransactionTransformer {
 		layout.add("signature", TransformationType.SIGNATURE);
 	}
 
-	static TransactionData fromByteBuffer(ByteBuffer byteBuffer) throws TransformationException {
+	public static TransactionData fromByteBuffer(ByteBuffer byteBuffer) throws TransformationException {
 		long timestamp = byteBuffer.getLong();
 
 		byte[] reference = new byte[REFERENCE_LENGTH];