Cache transaction list for 2 minutes, and synchronize, to prevent the balance and transactions APIs both requesting at once.

This ensures that only a single round of requests (per coin) is used for the wallettransactions and balance APIs. It also speeds up loading on subsequent requests. The 2 minute cache isn't much longer than the foreign block times, so shouldn't cause values to be too out of date.
This commit is contained in:
CalDescent 2022-02-03 21:04:49 +00:00
parent 892612c084
commit 9224ffbf73

View File

@ -39,6 +39,7 @@ import org.qortal.utils.Amounts;
import org.qortal.utils.BitTwiddling; import org.qortal.utils.BitTwiddling;
import com.google.common.hash.HashCode; import com.google.common.hash.HashCode;
import org.qortal.utils.NTP;
/** Bitcoin-like (Bitcoin, Litecoin, etc.) support */ /** Bitcoin-like (Bitcoin, Litecoin, etc.) support */
public abstract class Bitcoiny implements ForeignBlockchain { public abstract class Bitcoiny implements ForeignBlockchain {
@ -53,6 +54,11 @@ public abstract class Bitcoiny implements ForeignBlockchain {
protected final NetworkParameters params; protected final NetworkParameters params;
/** Cache recent transactions to speed up subsequent lookups */
protected List<SimpleTransaction> transactionsCache;
protected Long transactionsCacheTimestamp;
protected static long TRANSACTIONS_CACHE_TIMEOUT = 2 * 60 * 1000L; // 2 minutes
/** Keys that have been previously marked as fully spent,<br> /** Keys that have been previously marked as fully spent,<br>
* i.e. keys with transactions but with no unspent outputs. */ * i.e. keys with transactions but with no unspent outputs. */
protected final Set<ECKey> spentKeys = Collections.synchronizedSet(new HashSet<>()); protected final Set<ECKey> spentKeys = Collections.synchronizedSet(new HashSet<>());
@ -353,69 +359,87 @@ public abstract class Bitcoiny implements ForeignBlockchain {
} }
public List<SimpleTransaction> getWalletTransactions(String key58) throws ForeignBlockchainException { public List<SimpleTransaction> getWalletTransactions(String key58) throws ForeignBlockchainException {
Context.propagate(bitcoinjContext); synchronized (this) {
// Serve from the cache if it's recent
Wallet wallet = walletFromDeterministicKey58(key58); if (transactionsCache != null && transactionsCacheTimestamp != null) {
DeterministicKeyChain keyChain = wallet.getActiveKeyChain(); Long now = NTP.getTime();
boolean isCacheStale = (now != null && now - transactionsCacheTimestamp >= TRANSACTIONS_CACHE_TIMEOUT);
keyChain.setLookaheadSize(Bitcoiny.WALLET_KEY_LOOKAHEAD_INCREMENT); if (!isCacheStale) {
keyChain.maybeLookAhead(); LOGGER.info("Serving transactions from cache");
return transactionsCache;
List<DeterministicKey> keys = new ArrayList<>(keyChain.getLeafKeys());
Set<BitcoinyTransaction> walletTransactions = new HashSet<>();
Set<String> keySet = new HashSet<>();
// Set the number of consecutive empty batches required before giving up
final int numberOfAdditionalBatchesToSearch = 5;
int unusedCounter = 0;
int ki = 0;
do {
boolean areAllKeysUnused = true;
for (; ki < keys.size(); ++ki) {
DeterministicKey dKey = keys.get(ki);
// Check for transactions
Address address = Address.fromKey(this.params, dKey, ScriptType.P2PKH);
keySet.add(address.toString());
byte[] script = ScriptBuilder.createOutputScript(address).getProgram();
// Ask for transaction history - if it's empty then key has never been used
List<TransactionHash> historicTransactionHashes = this.blockchain.getAddressTransactions(script, false);
if (!historicTransactionHashes.isEmpty()) {
areAllKeysUnused = false;
for (TransactionHash transactionHash : historicTransactionHashes)
walletTransactions.add(this.getTransaction(transactionHash.txHash));
} }
} }
LOGGER.info("Fetching transactions from ElectrumX");
if (areAllKeysUnused) { Context.propagate(bitcoinjContext);
// No transactions
if (unusedCounter >= numberOfAdditionalBatchesToSearch) { Wallet wallet = walletFromDeterministicKey58(key58);
// ... and we've hit our search limit DeterministicKeyChain keyChain = wallet.getActiveKeyChain();
break;
keyChain.setLookaheadSize(Bitcoiny.WALLET_KEY_LOOKAHEAD_INCREMENT);
keyChain.maybeLookAhead();
List<DeterministicKey> keys = new ArrayList<>(keyChain.getLeafKeys());
Set<BitcoinyTransaction> walletTransactions = new HashSet<>();
Set<String> keySet = new HashSet<>();
// Set the number of consecutive empty batches required before giving up
final int numberOfAdditionalBatchesToSearch = 5;
int unusedCounter = 0;
int ki = 0;
do {
boolean areAllKeysUnused = true;
for (; ki < keys.size(); ++ki) {
DeterministicKey dKey = keys.get(ki);
// Check for transactions
Address address = Address.fromKey(this.params, dKey, ScriptType.P2PKH);
keySet.add(address.toString());
byte[] script = ScriptBuilder.createOutputScript(address).getProgram();
// Ask for transaction history - if it's empty then key has never been used
List<TransactionHash> historicTransactionHashes = this.blockchain.getAddressTransactions(script, false);
if (!historicTransactionHashes.isEmpty()) {
areAllKeysUnused = false;
for (TransactionHash transactionHash : historicTransactionHashes)
walletTransactions.add(this.getTransaction(transactionHash.txHash));
}
} }
// We haven't hit our search limit yet so increment the counter and keep looking
unusedCounter++;
}
else {
// Some keys in this batch were used, so reset the counter
unusedCounter = 0;
}
// Generate some more keys if (areAllKeysUnused) {
keys.addAll(generateMoreKeys(keyChain)); // No transactions
if (unusedCounter >= numberOfAdditionalBatchesToSearch) {
// ... and we've hit our search limit
break;
}
// We haven't hit our search limit yet so increment the counter and keep looking
unusedCounter++;
} else {
// Some keys in this batch were used, so reset the counter
unusedCounter = 0;
}
// Process new keys // Generate some more keys
} while (true); keys.addAll(generateMoreKeys(keyChain));
Comparator<SimpleTransaction> newestTimestampFirstComparator = Comparator.comparingInt(SimpleTransaction::getTimestamp).reversed(); // Process new keys
} while (true);
return walletTransactions.stream().map(t -> convertToSimpleTransaction(t, keySet)).sorted(newestTimestampFirstComparator).collect(Collectors.toList()); Comparator<SimpleTransaction> newestTimestampFirstComparator = Comparator.comparingInt(SimpleTransaction::getTimestamp).reversed();
// Update cache and return
transactionsCacheTimestamp = NTP.getTime();
transactionsCache = walletTransactions.stream()
.map(t -> convertToSimpleTransaction(t, keySet))
.sorted(newestTimestampFirstComparator).collect(Collectors.toList());
return transactionsCache;
}
} }
protected SimpleTransaction convertToSimpleTransaction(BitcoinyTransaction t, Set<String> keySet) { protected SimpleTransaction convertToSimpleTransaction(BitcoinyTransaction t, Set<String> keySet) {