forked from Qortal/qortal
Potential HSQLDB deadlock fix
After opening repository connection with RepositoryManager.getRepostory(), any 'read' from repository (e.g. SELECT) starts the transaction even though HSQLDB documentation states there are no shared/read locks in MVCC concurrency model. The work-around for this is to 'reset' HSQLDB's in-transaction flag by performing a ROLLBACK (Repository.discardChanges) immediately after acquiring the blockchain lock (which is used to ringfence changes that might collide like these). Also adding an extra check to prevent payments to nonexistent AT addresses as it touches Transaction.
This commit is contained in:
parent
b21ef18533
commit
a316b8a810
@ -165,7 +165,8 @@ public class Account {
|
|||||||
/**
|
/**
|
||||||
* Fetch last reference for account, considering unconfirmed transactions only, or return null.
|
* Fetch last reference for account, considering unconfirmed transactions only, or return null.
|
||||||
* <p>
|
* <p>
|
||||||
* NOTE: a repository savepoint may be used during execution.
|
* NOTE: calls Transaction.getUnconfirmedTransactions which discards uncommitted
|
||||||
|
* repository changes.
|
||||||
*
|
*
|
||||||
* @return byte[] reference, or null if no unconfirmed transactions for this account.
|
* @return byte[] reference, or null if no unconfirmed transactions for this account.
|
||||||
* @throws DataException
|
* @throws DataException
|
||||||
|
@ -78,8 +78,12 @@ public class AddressesResource {
|
|||||||
else {
|
else {
|
||||||
// Unconfirmed transactions could update lastReference
|
// Unconfirmed transactions could update lastReference
|
||||||
Account account = new Account(repository, address);
|
Account account = new Account(repository, address);
|
||||||
|
|
||||||
|
// Use last reference based on unconfirmed transactions if possible
|
||||||
byte[] unconfirmedLastReference = account.getUnconfirmedLastReference();
|
byte[] unconfirmedLastReference = account.getUnconfirmedLastReference();
|
||||||
|
|
||||||
if (unconfirmedLastReference != null)
|
if (unconfirmedLastReference != null)
|
||||||
|
// There are unconfirmed transactions so modify returned data
|
||||||
accountData.setReference(unconfirmedLastReference);
|
accountData.setReference(unconfirmedLastReference);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -112,8 +116,12 @@ public class AddressesResource {
|
|||||||
|
|
||||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||||
Account account = new Account(repository, address);
|
Account account = new Account(repository, address);
|
||||||
|
|
||||||
|
// Use last reference based on unconfirmed transactions if possible
|
||||||
lastReference = account.getUnconfirmedLastReference();
|
lastReference = account.getUnconfirmedLastReference();
|
||||||
|
|
||||||
if (lastReference == null)
|
if (lastReference == null)
|
||||||
|
// No unconfirmed transactions so fallback to using one save in account data
|
||||||
lastReference = account.getLastReference();
|
lastReference = account.getLastReference();
|
||||||
} catch (ApiException e) {
|
} catch (ApiException e) {
|
||||||
throw e;
|
throw e;
|
||||||
|
@ -96,6 +96,9 @@ public class BlockGenerator extends Thread {
|
|||||||
ReentrantLock blockchainLock = Controller.getInstance().getBlockchainLock();
|
ReentrantLock blockchainLock = Controller.getInstance().getBlockchainLock();
|
||||||
if (blockchainLock.tryLock())
|
if (blockchainLock.tryLock())
|
||||||
generation: try {
|
generation: try {
|
||||||
|
// Clear repository's "in transaction" state so we don't cause a repository deadlock
|
||||||
|
repository.discardChanges();
|
||||||
|
|
||||||
List<Block> goodBlocks = new ArrayList<>();
|
List<Block> goodBlocks = new ArrayList<>();
|
||||||
|
|
||||||
for (Block testBlock : newBlocks) {
|
for (Block testBlock : newBlocks) {
|
||||||
@ -118,7 +121,7 @@ public class BlockGenerator extends Thread {
|
|||||||
int winningIndex = new Random().nextInt(goodBlocks.size());
|
int winningIndex = new Random().nextInt(goodBlocks.size());
|
||||||
Block newBlock = goodBlocks.get(winningIndex);
|
Block newBlock = goodBlocks.get(winningIndex);
|
||||||
|
|
||||||
// Delete invalid transactions
|
// Delete invalid transactions. NOTE: discards repository changes on entry, saves changes on exit.
|
||||||
deleteInvalidTransactions(repository);
|
deleteInvalidTransactions(repository);
|
||||||
|
|
||||||
// Add unconfirmed transactions
|
// Add unconfirmed transactions
|
||||||
@ -167,6 +170,17 @@ public class BlockGenerator extends Thread {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes invalid, unconfirmed transactions from repository.
|
||||||
|
* <p>
|
||||||
|
* NOTE: calls Transaction.getInvalidTransactions which discards uncommitted
|
||||||
|
* repository changes.
|
||||||
|
* <p>
|
||||||
|
* Also commits the deletion of invalid transactions to the repository.
|
||||||
|
*
|
||||||
|
* @param repository
|
||||||
|
* @throws DataException
|
||||||
|
*/
|
||||||
private static void deleteInvalidTransactions(Repository repository) throws DataException {
|
private static void deleteInvalidTransactions(Repository repository) throws DataException {
|
||||||
List<TransactionData> invalidTransactions = Transaction.getInvalidTransactions(repository);
|
List<TransactionData> invalidTransactions = Transaction.getInvalidTransactions(repository);
|
||||||
|
|
||||||
@ -175,9 +189,20 @@ public class BlockGenerator extends Thread {
|
|||||||
LOGGER.trace(String.format("Deleting invalid, unconfirmed transaction %s", Base58.encode(invalidTransactionData.getSignature())));
|
LOGGER.trace(String.format("Deleting invalid, unconfirmed transaction %s", Base58.encode(invalidTransactionData.getSignature())));
|
||||||
repository.getTransactionRepository().delete(invalidTransactionData);
|
repository.getTransactionRepository().delete(invalidTransactionData);
|
||||||
}
|
}
|
||||||
|
|
||||||
repository.saveChanges();
|
repository.saveChanges();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds unconfirmed transactions to passed block.
|
||||||
|
* <p>
|
||||||
|
* NOTE: calls Transaction.getUnconfirmedTransactions which discards uncommitted
|
||||||
|
* repository changes.
|
||||||
|
*
|
||||||
|
* @param repository
|
||||||
|
* @param newBlock
|
||||||
|
* @throws DataException
|
||||||
|
*/
|
||||||
private static void addUnconfirmedTransactions(Repository repository, Block newBlock) throws DataException {
|
private static void addUnconfirmedTransactions(Repository repository, Block newBlock) throws DataException {
|
||||||
// Grab all valid unconfirmed transactions (already sorted)
|
// Grab all valid unconfirmed transactions (already sorted)
|
||||||
List<TransactionData> unconfirmedTransactions = Transaction.getUnconfirmedTransactions(repository);
|
List<TransactionData> unconfirmedTransactions = Transaction.getUnconfirmedTransactions(repository);
|
||||||
|
@ -90,6 +90,17 @@ public class Crypto {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static boolean isValidAddress(String address) {
|
public static boolean isValidAddress(String address) {
|
||||||
|
return isValidTypedAddress(address, ADDRESS_VERSION, AT_ADDRESS_VERSION);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean isValidAtAddress(String address) {
|
||||||
|
return isValidTypedAddress(address, AT_ADDRESS_VERSION);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean isValidTypedAddress(String address, byte...addressVersions) {
|
||||||
|
if (addressVersions == null || addressVersions.length == 0)
|
||||||
|
return false;
|
||||||
|
|
||||||
byte[] addressBytes;
|
byte[] addressBytes;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@ -104,18 +115,16 @@ public class Crypto {
|
|||||||
return false;
|
return false;
|
||||||
|
|
||||||
// Check by address type
|
// Check by address type
|
||||||
switch (addressBytes[0]) {
|
for (byte addressVersion : addressVersions)
|
||||||
case ADDRESS_VERSION:
|
if (addressBytes[0] == addressVersion) {
|
||||||
case AT_ADDRESS_VERSION:
|
|
||||||
byte[] addressWithoutChecksum = Arrays.copyOf(addressBytes, addressBytes.length - 4);
|
byte[] addressWithoutChecksum = Arrays.copyOf(addressBytes, addressBytes.length - 4);
|
||||||
byte[] passedChecksum = Arrays.copyOfRange(addressBytes, addressBytes.length - 4, addressBytes.length);
|
byte[] passedChecksum = Arrays.copyOfRange(addressBytes, addressBytes.length - 4, addressBytes.length);
|
||||||
|
|
||||||
byte[] generatedChecksum = Arrays.copyOf(doubleDigest(addressWithoutChecksum), 4);
|
byte[] generatedChecksum = Arrays.copyOf(doubleDigest(addressWithoutChecksum), 4);
|
||||||
return Arrays.equals(passedChecksum, generatedChecksum);
|
return Arrays.equals(passedChecksum, generatedChecksum);
|
||||||
|
}
|
||||||
|
|
||||||
default:
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -60,10 +60,19 @@ public class Payment {
|
|||||||
if (!Crypto.isValidAddress(paymentData.getRecipient()))
|
if (!Crypto.isValidAddress(paymentData.getRecipient()))
|
||||||
return ValidationResult.INVALID_ADDRESS;
|
return ValidationResult.INVALID_ADDRESS;
|
||||||
|
|
||||||
// Do not allow payments to finished/dead ATs
|
boolean recipientIsAT = Crypto.isValidAtAddress(paymentData.getRecipient());
|
||||||
ATData atData = this.repository.getATRepository().fromATAddress(paymentData.getRecipient());
|
ATData atData = null;
|
||||||
|
|
||||||
|
// Do not allow payments to finished/dead/nonexistent ATs
|
||||||
|
if (recipientIsAT) {
|
||||||
|
atData = this.repository.getATRepository().fromATAddress(paymentData.getRecipient());
|
||||||
|
|
||||||
|
if (atData == null)
|
||||||
|
return ValidationResult.AT_UNKNOWN;
|
||||||
|
|
||||||
if (atData != null && atData.getIsFinished())
|
if (atData != null && atData.getIsFinished())
|
||||||
return ValidationResult.AT_IS_FINISHED;
|
return ValidationResult.AT_IS_FINISHED;
|
||||||
|
}
|
||||||
|
|
||||||
AssetData assetData = assetRepository.fromAssetId(paymentData.getAssetId());
|
AssetData assetData = assetRepository.fromAssetId(paymentData.getAssetId());
|
||||||
// Check asset even exists
|
// Check asset even exists
|
||||||
|
@ -242,7 +242,7 @@ public class HSQLDBRepository implements Repository {
|
|||||||
if (this.sqlStatements == null)
|
if (this.sqlStatements == null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
LOGGER.info("HSQLDB SQL statements leading up to this were:");
|
LOGGER.info(String.format("HSQLDB SQL statements (session %d) leading up to this were:", this.sessionId));
|
||||||
|
|
||||||
for (String sql : this.sqlStatements)
|
for (String sql : this.sqlStatements)
|
||||||
LOGGER.info(sql);
|
LOGGER.info(sql);
|
||||||
@ -449,7 +449,7 @@ public class HSQLDBRepository implements Repository {
|
|||||||
|
|
||||||
/** Logs other HSQLDB sessions then re-throws passed exception */
|
/** Logs other HSQLDB sessions then re-throws passed exception */
|
||||||
public SQLException examineException(SQLException e) throws SQLException {
|
public SQLException examineException(SQLException e) throws SQLException {
|
||||||
LOGGER.error("SQL error: " + e.getMessage(), e);
|
LOGGER.error(String.format("HSQLDB error (session %d): %s", this.sessionId, e.getMessage()), e);
|
||||||
|
|
||||||
logStatements();
|
logStatements();
|
||||||
|
|
||||||
|
@ -203,6 +203,7 @@ public abstract class Transaction {
|
|||||||
INVALID_FORGE_SHARE(77),
|
INVALID_FORGE_SHARE(77),
|
||||||
PUBLIC_KEY_UNKNOWN(78),
|
PUBLIC_KEY_UNKNOWN(78),
|
||||||
INVALID_PUBLIC_KEY(79),
|
INVALID_PUBLIC_KEY(79),
|
||||||
|
AT_UNKNOWN(80),
|
||||||
NOT_YET_RELEASED(1000);
|
NOT_YET_RELEASED(1000);
|
||||||
|
|
||||||
public final int value;
|
public final int value;
|
||||||
@ -472,12 +473,8 @@ public abstract class Transaction {
|
|||||||
/**
|
/**
|
||||||
* Returns whether transaction can be added to unconfirmed transactions.
|
* Returns whether transaction can be added to unconfirmed transactions.
|
||||||
* <p>
|
* <p>
|
||||||
* NOTE: temporarily updates creator's lastReference to that from
|
* NOTE: temporarily updates accounts' lastReference to check validity.<br>
|
||||||
* unconfirmed transactions, and hence uses a repository savepoint.
|
* To do this, blockchain lock is obtained and pending repository changes are discarded.
|
||||||
* <p>
|
|
||||||
* This is not done normally because we don't want unconfirmed transactions affecting validity of transactions already included in a block.
|
|
||||||
* <p>
|
|
||||||
* Also temporarily acquires blockchain lock.
|
|
||||||
*
|
*
|
||||||
* @return true if transaction can be added to unconfirmed transactions, false otherwise
|
* @return true if transaction can be added to unconfirmed transactions, false otherwise
|
||||||
* @throws DataException
|
* @throws DataException
|
||||||
@ -500,12 +497,14 @@ public abstract class Transaction {
|
|||||||
/*
|
/*
|
||||||
* We have to grab the blockchain lock because we're updating
|
* We have to grab the blockchain lock because we're updating
|
||||||
* when we fake the creator's last reference,
|
* when we fake the creator's last reference,
|
||||||
* even though we throw away the update when we rollback the
|
* even though we throw away the update when we discard changes.
|
||||||
* savepoint.
|
|
||||||
*/
|
*/
|
||||||
ReentrantLock blockchainLock = Controller.getInstance().getBlockchainLock();
|
ReentrantLock blockchainLock = Controller.getInstance().getBlockchainLock();
|
||||||
blockchainLock.lock();
|
blockchainLock.lock();
|
||||||
repository.setSavepoint();
|
try {
|
||||||
|
// Clear repository's "in transaction" state so we don't cause a repository deadlock
|
||||||
|
repository.discardChanges();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
PublicKeyAccount creator = this.getCreator();
|
PublicKeyAccount creator = this.getCreator();
|
||||||
if (creator == null)
|
if (creator == null)
|
||||||
@ -527,7 +526,10 @@ public abstract class Transaction {
|
|||||||
|
|
||||||
return result;
|
return result;
|
||||||
} finally {
|
} finally {
|
||||||
repository.rollbackToSavepoint();
|
repository.discardChanges();
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
// In separate finally block just in case rollback throws
|
||||||
blockchainLock.unlock();
|
blockchainLock.unlock();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -576,12 +578,10 @@ public abstract class Transaction {
|
|||||||
/**
|
/**
|
||||||
* Returns sorted, unconfirmed transactions, excluding invalid.
|
* Returns sorted, unconfirmed transactions, excluding invalid.
|
||||||
* <p>
|
* <p>
|
||||||
* NOTE: temporarily updates accounts' lastReference to that from
|
* NOTE: temporarily updates accounts' lastReference to check validity.<br>
|
||||||
* unconfirmed transactions, hence uses a repository savepoint.
|
* To do this, blockchain lock is obtained and pending repository changes are discarded.
|
||||||
* <p>
|
|
||||||
* Also temporarily acquires blockchain lock.
|
|
||||||
*
|
*
|
||||||
* @return sorted unconfirmed transactions
|
* @return sorted, unconfirmed transactions
|
||||||
* @throws DataException
|
* @throws DataException
|
||||||
*/
|
*/
|
||||||
public static List<TransactionData> getUnconfirmedTransactions(Repository repository) throws DataException {
|
public static List<TransactionData> getUnconfirmedTransactions(Repository repository) throws DataException {
|
||||||
@ -599,7 +599,10 @@ public abstract class Transaction {
|
|||||||
*/
|
*/
|
||||||
ReentrantLock blockchainLock = Controller.getInstance().getBlockchainLock();
|
ReentrantLock blockchainLock = Controller.getInstance().getBlockchainLock();
|
||||||
blockchainLock.lock();
|
blockchainLock.lock();
|
||||||
repository.setSavepoint();
|
try {
|
||||||
|
// Clear repository's "in transaction" state so we don't cause a repository deadlock
|
||||||
|
repository.discardChanges();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
for (int i = 0; i < unconfirmedTransactions.size(); ++i) {
|
for (int i = 0; i < unconfirmedTransactions.size(); ++i) {
|
||||||
TransactionData transactionData = unconfirmedTransactions.get(i);
|
TransactionData transactionData = unconfirmedTransactions.get(i);
|
||||||
@ -612,7 +615,10 @@ public abstract class Transaction {
|
|||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
// Throw away temporary updates to account lastReference
|
// Throw away temporary updates to account lastReference
|
||||||
repository.rollbackToSavepoint();
|
repository.discardChanges();
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
// In separate finally block just in case rollback throws
|
||||||
blockchainLock.unlock();
|
blockchainLock.unlock();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -622,12 +628,10 @@ public abstract class Transaction {
|
|||||||
/**
|
/**
|
||||||
* Returns invalid, unconfirmed transactions.
|
* Returns invalid, unconfirmed transactions.
|
||||||
* <p>
|
* <p>
|
||||||
* NOTE: temporarily updates accounts' lastReference to that from
|
* NOTE: temporarily updates accounts' lastReference to check validity.<br>
|
||||||
* unconfirmed transactions, hence uses a repository savepoint.
|
* To do this, blockchain lock is obtained and pending repository changes are discarded.
|
||||||
* <p>
|
|
||||||
* Also temporarily acquires blockchain lock.
|
|
||||||
*
|
*
|
||||||
* @return sorted unconfirmed transactions
|
* @return sorted, invalid, unconfirmed transactions
|
||||||
* @throws DataException
|
* @throws DataException
|
||||||
*/
|
*/
|
||||||
public static List<TransactionData> getInvalidTransactions(Repository repository) throws DataException {
|
public static List<TransactionData> getInvalidTransactions(Repository repository) throws DataException {
|
||||||
@ -646,7 +650,10 @@ public abstract class Transaction {
|
|||||||
*/
|
*/
|
||||||
ReentrantLock blockchainLock = Controller.getInstance().getBlockchainLock();
|
ReentrantLock blockchainLock = Controller.getInstance().getBlockchainLock();
|
||||||
blockchainLock.lock();
|
blockchainLock.lock();
|
||||||
repository.setSavepoint();
|
try {
|
||||||
|
// Clear repository's "in transaction" state so we don't cause a repository deadlock
|
||||||
|
repository.discardChanges();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
for (int i = 0; i < unconfirmedTransactions.size(); ++i) {
|
for (int i = 0; i < unconfirmedTransactions.size(); ++i) {
|
||||||
TransactionData transactionData = unconfirmedTransactions.get(i);
|
TransactionData transactionData = unconfirmedTransactions.get(i);
|
||||||
@ -661,7 +668,10 @@ public abstract class Transaction {
|
|||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
// Throw away temporary updates to account lastReference
|
// Throw away temporary updates to account lastReference
|
||||||
repository.rollbackToSavepoint();
|
repository.discardChanges();
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
// In separate finally block just in case rollback throws
|
||||||
blockchainLock.unlock();
|
blockchainLock.unlock();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -709,6 +719,7 @@ public abstract class Transaction {
|
|||||||
// These updates should be discarded by some caller further up stack
|
// These updates should be discarded by some caller further up stack
|
||||||
PublicKeyAccount creator = new PublicKeyAccount(repository, transactionData.getCreatorPublicKey());
|
PublicKeyAccount creator = new PublicKeyAccount(repository, transactionData.getCreatorPublicKey());
|
||||||
creator.setLastReference(transactionData.getSignature());
|
creator.setLastReference(transactionData.getSignature());
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
package org.qora.test;
|
package org.qora.test;
|
||||||
|
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
|
import org.qora.account.Account;
|
||||||
|
import org.qora.asset.Asset;
|
||||||
import org.qora.repository.DataException;
|
import org.qora.repository.DataException;
|
||||||
import org.qora.repository.Repository;
|
import org.qora.repository.Repository;
|
||||||
import org.qora.repository.RepositoryManager;
|
import org.qora.repository.RepositoryManager;
|
||||||
@ -8,6 +10,8 @@ import org.qora.test.common.Common;
|
|||||||
|
|
||||||
import static org.junit.Assert.*;
|
import static org.junit.Assert.*;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
|
||||||
import org.apache.logging.log4j.LogManager;
|
import org.apache.logging.log4j.LogManager;
|
||||||
import org.apache.logging.log4j.Logger;
|
import org.apache.logging.log4j.Logger;
|
||||||
|
|
||||||
@ -55,4 +59,31 @@ public class RepositoryTests extends Common {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testDeadlock() throws DataException {
|
||||||
|
Common.useDefaultSettings();
|
||||||
|
|
||||||
|
// Open connection 1
|
||||||
|
try (Repository repository1 = RepositoryManager.getRepository()) {
|
||||||
|
|
||||||
|
// Do a database 'read'
|
||||||
|
Account account1 = Common.getTestAccount(repository1, "alice");
|
||||||
|
account1.getLastReference();
|
||||||
|
|
||||||
|
// Open connection 2
|
||||||
|
try (Repository repository2 = RepositoryManager.getRepository()) {
|
||||||
|
// Update account in 2
|
||||||
|
Account account2 = Common.getTestAccount(repository2, "alice");
|
||||||
|
account2.setConfirmedBalance(Asset.QORA, BigDecimal.valueOf(1234L));
|
||||||
|
repository2.saveChanges();
|
||||||
|
}
|
||||||
|
|
||||||
|
repository1.discardChanges();
|
||||||
|
|
||||||
|
// Update account in 1
|
||||||
|
account1.setConfirmedBalance(Asset.QORA, BigDecimal.valueOf(5678L));
|
||||||
|
repository1.saveChanges();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user