Interim commit with new AccountRefCache, but no tests and no Block support

This commit is contained in:
catbref 2020-04-25 17:14:12 +01:00
parent 40531284dd
commit e141e98ecc
6 changed files with 237 additions and 113 deletions

View File

@ -1,7 +1,6 @@
package org.qortal.account; package org.qortal.account;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.util.List;
import javax.xml.bind.annotation.XmlAccessType; import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType; import javax.xml.bind.annotation.XmlAccessorType;
@ -12,10 +11,8 @@ import org.qortal.block.BlockChain;
import org.qortal.data.account.AccountBalanceData; import org.qortal.data.account.AccountBalanceData;
import org.qortal.data.account.AccountData; import org.qortal.data.account.AccountData;
import org.qortal.data.account.RewardShareData; import org.qortal.data.account.RewardShareData;
import org.qortal.data.transaction.TransactionData;
import org.qortal.repository.DataException; import org.qortal.repository.DataException;
import org.qortal.repository.Repository; import org.qortal.repository.Repository;
import org.qortal.transaction.Transaction;
import org.qortal.utils.Base58; import org.qortal.utils.Base58;
@XmlAccessorType(XmlAccessType.NONE) // Stops JAX-RS errors when unmarshalling blockchain config @XmlAccessorType(XmlAccessType.NONE) // Stops JAX-RS errors when unmarshalling blockchain config
@ -109,38 +106,11 @@ public class Account {
* @throws DataException * @throws DataException
*/ */
public byte[] getLastReference() throws DataException { public byte[] getLastReference() throws DataException {
byte[] reference = this.repository.getAccountRepository().getLastReference(this.address); byte[] reference = AccountRefCache.getLastReference(this.repository, this.address);
LOGGER.trace(() -> String.format("Last reference for %s is %s", this.address, reference == null ? "null" : Base58.encode(reference))); LOGGER.trace(() -> String.format("Last reference for %s is %s", this.address, reference == null ? "null" : Base58.encode(reference)));
return reference; return reference;
} }
/**
* Fetch last reference for account, considering unconfirmed transactions only, or return null.
* <p>
* NOTE: calls Transaction.getUnconfirmedTransactions which discards uncommitted
* repository changes.
*
* @return byte[] reference, or null if no unconfirmed transactions for this account.
* @throws DataException
*/
public byte[] getUnconfirmedLastReference() throws DataException {
// Newest unconfirmed transaction takes priority
List<TransactionData> unconfirmedTransactions = Transaction.getUnconfirmedTransactions(repository);
byte[] reference = null;
for (TransactionData transactionData : unconfirmedTransactions) {
String unconfirmedTransactionAddress = PublicKeyAccount.getAddress(transactionData.getCreatorPublicKey());
if (unconfirmedTransactionAddress.equals(this.address))
reference = transactionData.getSignature();
}
final byte[] loggingReference = reference;
LOGGER.trace(() -> String.format("Last unconfirmed reference for %s is %s", this.address, loggingReference == null ? "null" : Base58.encode(loggingReference)));
return reference;
}
/** /**
* Set last reference for account. * Set last reference for account.
* *
@ -153,7 +123,7 @@ public class Account {
AccountData accountData = this.buildAccountData(); AccountData accountData = this.buildAccountData();
accountData.setReference(reference); accountData.setReference(reference);
this.repository.getAccountRepository().setLastReference(accountData); AccountRefCache.setLastReference(this.repository, accountData);
} }
// Default groupID manipulations // Default groupID manipulations

View File

@ -0,0 +1,124 @@
package org.qortal.account;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;
import java.util.function.BiFunction;
import org.qortal.data.account.AccountData;
import org.qortal.repository.DataException;
import org.qortal.repository.Repository;
import org.qortal.utils.Pair;
public class AccountRefCache implements AutoCloseable {
private static final Map<Repository, RefCache> CACHE = new HashMap<Repository, RefCache>();
private static class RefCache {
private final Map<String, byte[]> getLastReferenceValues = new HashMap<String, byte[]>();
private final Map<String, Pair<byte[], byte[]>> setLastReferenceValues = new HashMap<String, Pair<byte[], byte[]>>();
public byte[] getLastReference(Repository repository, String address) throws DataException {
synchronized (this.getLastReferenceValues) {
byte[] lastReference = getLastReferenceValues.get(address);
if (lastReference != null)
// address is present in map, lastReference not null
return lastReference;
// address is present in map, just lastReference is null
if (getLastReferenceValues.containsKey(address))
return null;
lastReference = repository.getAccountRepository().getLastReference(address);
this.getLastReferenceValues.put(address, lastReference);
return lastReference;
}
}
public void setLastReference(AccountData accountData) {
BiFunction<String, Pair<byte[], byte[]>, Pair<byte[], byte[]>> mergePublicKey = (key, oldPair) -> {
byte[] mergedPublicKey = accountData.getPublicKey() != null ? accountData.getPublicKey() : oldPair.getB();
return new Pair<>(accountData.getReference(), mergedPublicKey);
};
synchronized (this.setLastReferenceValues) {
setLastReferenceValues.computeIfPresent(accountData.getAddress(), mergePublicKey);
}
}
Map<String, Pair<byte[], byte[]>> getNewLastReferences() {
return setLastReferenceValues;
}
}
private Repository repository;
public AccountRefCache(Repository repository) {
RefCache refCache = new RefCache();
synchronized (CACHE) {
if (CACHE.putIfAbsent(repository, refCache) != null)
throw new IllegalStateException("Account reference cache entry already exists");
}
this.repository = repository;
}
public void commit() throws DataException {
RefCache refCache;
synchronized (CACHE) {
refCache = CACHE.remove(this.repository);
}
if (refCache == null)
throw new IllegalStateException("Tried to commit non-existent account reference cache");
Map<String, Pair<byte[], byte[]>> newLastReferenceValues = refCache.getNewLastReferences();
for (Entry<String, Pair<byte[], byte[]>> entry : newLastReferenceValues.entrySet()) {
AccountData accountData = new AccountData(entry.getKey());
accountData.setReference(entry.getValue().getA());
if (entry.getValue().getB() != null)
accountData.setPublicKey(entry.getValue().getB());
this.repository.getAccountRepository().setLastReference(accountData);
}
}
@Override
public void close() throws Exception {
synchronized (CACHE) {
CACHE.remove(this.repository);
}
}
/*package*/ static byte[] getLastReference(Repository repository, String address) throws DataException {
RefCache refCache;
synchronized (CACHE) {
refCache = CACHE.get(repository);
}
if (refCache == null)
return repository.getAccountRepository().getLastReference(address);
return refCache.getLastReference(repository, address);
}
/*package*/ static void setLastReference(Repository repository, AccountData accountData) throws DataException {
RefCache refCache;
synchronized (CACHE) {
refCache = CACHE.get(repository);
}
if (refCache == null)
repository.getAccountRepository().setLastReference(accountData);
refCache.setLastReference(accountData);
}
}

View File

@ -66,32 +66,18 @@ public class AddressesResource {
) )
} }
) )
@ApiErrors({ApiError.INVALID_ADDRESS, ApiError.REPOSITORY_ISSUE}) @ApiErrors({ApiError.INVALID_ADDRESS, ApiError.ADDRESS_UNKNOWN, ApiError.REPOSITORY_ISSUE})
public AccountData getAccountInfo(@PathParam("address") String address) { public AccountData getAccountInfo(@PathParam("address") String address) {
if (!Crypto.isValidAddress(address)) if (!Crypto.isValidAddress(address))
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS); throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS);
try (final Repository repository = RepositoryManager.getRepository()) { try (final Repository repository = RepositoryManager.getRepository()) {
AccountData accountData = repository.getAccountRepository().getAccount(address); AccountData accountData = repository.getAccountRepository().getAccount(address);
// Not found? // Not found?
if (accountData == null) if (accountData == null)
accountData = new AccountData(address); throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.ADDRESS_UNKNOWN);
else {
// Unconfirmed transactions could update lastReference
Account account = new Account(repository, address);
// Use last reference based on unconfirmed transactions if possible
byte[] unconfirmedLastReference = account.getUnconfirmedLastReference();
if (unconfirmedLastReference != null)
// There are unconfirmed transactions so modify returned data
accountData.setReference(unconfirmedLastReference);
}
return accountData; return accountData;
} catch (ApiException e) {
throw e;
} catch (DataException e) { } catch (DataException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e); throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
} }
@ -100,42 +86,37 @@ public class AddressesResource {
@GET @GET
@Path("/lastreference/{address}") @Path("/lastreference/{address}")
@Operation( @Operation(
summary = "Fetch reference for next transaction to be created by address, considering unconfirmed transactions", summary = "Fetch reference for next transaction to be created by address",
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.", description = "Returns the base58-encoded signature of the last confirmed transaction created by address, failing that: the first incoming transaction. Returns \"false\" if there is no last-reference.",
responses = { responses = {
@ApiResponse( @ApiResponse(
description = "the base58-encoded transaction signature", description = "the base58-encoded last-reference",
content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(type = "string")) content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(type = "string"))
) )
} }
) )
@ApiErrors({ApiError.INVALID_ADDRESS, ApiError.REPOSITORY_ISSUE}) @ApiErrors({ApiError.INVALID_ADDRESS, ApiError.ADDRESS_UNKNOWN, ApiError.REPOSITORY_ISSUE})
public String getLastReferenceUnconfirmed(@PathParam("address") String address) { public String getLastReference(@PathParam("address") String address) {
if (!Crypto.isValidAddress(address)) if (!Crypto.isValidAddress(address))
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS); throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS);
byte[] lastReference = null; byte[] lastReference = null;
try (final Repository repository = RepositoryManager.getRepository()) { try (final Repository repository = RepositoryManager.getRepository()) {
Account account = new Account(repository, address); AccountData accountData = repository.getAccountRepository().getAccount(address);
// Not found?
if (accountData == null)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.ADDRESS_UNKNOWN);
// Use last reference based on unconfirmed transactions if possible lastReference = accountData.getReference();
lastReference = account.getUnconfirmedLastReference();
if (lastReference == null)
// No unconfirmed transactions so fallback to using one save in account data
lastReference = account.getLastReference();
} catch (ApiException e) {
throw e;
} catch (DataException e) { } catch (DataException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e); throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
} }
if(lastReference == null || lastReference.length == 0) { if (lastReference == null || lastReference.length == 0)
return "false"; return "false";
} else {
return Base58.encode(lastReference); return Base58.encode(lastReference);
}
} }
@GET @GET

View File

@ -537,54 +537,31 @@ public abstract class Transaction {
if (feeValidationResult != ValidationResult.OK) if (feeValidationResult != ValidationResult.OK)
return feeValidationResult; return feeValidationResult;
/* PublicKeyAccount creator = this.getCreator();
* We have to grab the blockchain lock because we're updating if (creator == null)
* when we fake the creator's last reference, return ValidationResult.MISSING_CREATOR;
* even though we throw away the update when we discard changes.
*/
ReentrantLock blockchainLock = Controller.getInstance().getBlockchainLock();
blockchainLock.lock();
try {
// Clear repository's "in transaction" state so we don't cause a repository deadlock
repository.discardChanges();
try { // Reject if unconfirmed pile already has X transactions from same creator
PublicKeyAccount creator = this.getCreator(); if (countUnconfirmedByCreator(creator) >= Settings.getInstance().getMaxUnconfirmedPerAccount())
if (creator == null) return ValidationResult.TOO_MANY_UNCONFIRMED;
return ValidationResult.MISSING_CREATOR;
// Reject if unconfirmed pile already has X transactions from same creator // Check transaction's txGroupId
if (countUnconfirmedByCreator(creator) >= Settings.getInstance().getMaxUnconfirmedPerAccount()) if (!this.isValidTxGroupId())
return ValidationResult.TOO_MANY_UNCONFIRMED; return ValidationResult.INVALID_TX_GROUP_ID;
// Check transaction's txGroupId // Check transaction references
if (!this.isValidTxGroupId()) if (!this.hasValidReference())
return ValidationResult.INVALID_TX_GROUP_ID; return ValidationResult.INVALID_REFERENCE;
byte[] unconfirmedLastReference = creator.getUnconfirmedLastReference(); // Check transaction is valid
if (unconfirmedLastReference != null) ValidationResult result = this.isValid();
creator.setLastReference(unconfirmedLastReference); if (result != ValidationResult.OK)
return result;
// Check transaction is valid // Check transaction is processable
ValidationResult result = this.isValid(); result = this.isProcessable();
if (result != ValidationResult.OK)
return result;
// Check transaction references return result;
if (!this.hasValidReference())
return ValidationResult.INVALID_REFERENCE;
// Check transaction is processable
result = this.isProcessable();
return result;
} finally {
repository.discardChanges();
}
} finally {
// In separate finally block just in case rollback throws
blockchainLock.unlock();
}
} }
/** Returns whether transaction's fee is valid. Might be overriden in transaction subclasses. */ /** Returns whether transaction's fee is valid. Might be overriden in transaction subclasses. */

View File

@ -0,0 +1,76 @@
package org.qortal.test;
public class AccountRefCacheTests {
// Test no cache in play (existing account):
// fetch 1st ref
// generate 2nd ref and call Account.setLastReference
// fetch 3rd ref
// 3rd ref should match 2st ref
// Test no cache in play (no account):
// fetch 1st ref
// generate 2nd ref and call Account.setLastReference
// fetch 3rd ref
// 3rd ref should match 2st ref
// Test cache in play (existing account, no commit):
// fetch 1st ref
// begin caching
// fetch 2nd ref
// 2nd ref should match 1st ref
// generate 3rd ref and call Account.setLastReference
// fetch 4th ref
// 4th ref should match 1st ref
// discard cache
// fetch 5th ref
// 5th ref should match 1st ref
// Test cache in play (existing account, with commit):
// fetch 1st ref
// begin caching
// fetch 2nd ref
// 2nd ref should match 1st ref
// generate 3rd ref and call Account.setLastReference
// fetch 4th ref
// 4th ref should match 1st ref
// commit cache
// fetch 5th ref
// 5th ref should match 3rd ref
// Test cache in play (new account, no commit):
// fetch 1st ref (null)
// begin caching
// fetch 2nd ref
// 2nd ref should match 1st ref
// generate 3rd ref and call Account.setLastReference
// fetch 4th ref
// 4th ref should match 1st ref
// discard cache
// fetch 5th ref
// 5th ref should match 1st ref
// Test cache in play (new account, with commit):
// fetch 1st ref (null)
// begin caching
// fetch 2nd ref
// 2nd ref should match 1st ref
// generate 3rd ref and call Account.setLastReference
// fetch 4th ref
// 4th ref should match 1st ref
// commit cache
// fetch 5th ref
// 5th ref should match 3rd ref
// Test Block support
// fetch 1st ref for Alice
// generate new payment from Alice to new account Ellen
// generate another payment from Alice to new account Ellen
// mint block containing payments
// confirm Ellen's ref is 1st payment's sig
// confirm Alice's ref if 2nd payment's sig
// orphan block
// confirm Ellen's ref is null
// confirm Alice's ref matches 1st ref
}

View File

@ -13,11 +13,7 @@ public abstract class TestTransaction {
protected static final Random random = new Random(); protected static final Random random = new Random();
public static BaseTransactionData generateBase(PrivateKeyAccount account) throws DataException { public static BaseTransactionData generateBase(PrivateKeyAccount account) throws DataException {
byte[] lastReference = account.getUnconfirmedLastReference(); return new BaseTransactionData(System.currentTimeMillis(), Group.NO_GROUP, account.getLastReference(), account.getPublicKey(), BlockChain.getInstance().getUnitFee(), null);
if (lastReference == null)
lastReference = account.getLastReference();
return new BaseTransactionData(System.currentTimeMillis(), Group.NO_GROUP, lastReference, account.getPublicKey(), BlockChain.getInstance().getUnitFee(), null);
} }
} }