diff --git a/src/main/java/org/qortal/crosschain/Bitcoiny.java b/src/main/java/org/qortal/crosschain/Bitcoiny.java index 4a819209..a4f5a2af 100644 --- a/src/main/java/org/qortal/crosschain/Bitcoiny.java +++ b/src/main/java/org/qortal/crosschain/Bitcoiny.java @@ -55,6 +55,13 @@ public abstract class Bitcoiny implements ForeignBlockchain { protected Coin feePerKb; + /** + * Blockchain Cache + * + * To store blockchain data and reduce redundant RPCs to the ElectrumX servers + */ + private final BlockchainCache blockchainCache = new BlockchainCache(); + // Constructors and instance protected Bitcoiny(BitcoinyBlockchainProvider blockchainProvider, Context bitcoinjContext, String currencyCode, Coin feePerKb) { @@ -509,8 +516,22 @@ public abstract class Bitcoiny implements ForeignBlockchain { if (!historicTransactionHashes.isEmpty()) { areAllKeysUnused = false; - for (TransactionHash transactionHash : historicTransactionHashes) - walletTransactions.add(this.getTransaction(transactionHash.txHash)); + for (TransactionHash transactionHash : historicTransactionHashes) { + + Optional walletTransaction + = this.blockchainCache.getTransactionByHash( transactionHash.txHash ); + + // if the wallet transaction is already cached + if(walletTransaction.isPresent() ) { + walletTransactions.add( walletTransaction.get() ); + } + // otherwise get the transaction from the blockchain server + else { + BitcoinyTransaction transaction = getTransaction(transactionHash.txHash); + walletTransactions.add( transaction ); + this.blockchainCache.addTransactionByHash(transactionHash.txHash, transaction); + } + } } } @@ -602,17 +623,25 @@ public abstract class Bitcoiny implements ForeignBlockchain { 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 historicTransactionHashes = this.getAddressTransactions(script, true); - if (!historicTransactionHashes.isEmpty()) { + // if the key already has a verified transaction history + if( this.blockchainCache.keyHasHistory( dKey ) ){ areAllKeysUnused = false; } + else { + // Check for transactions + byte[] script = ScriptBuilder.createOutputScript(address).getProgram(); + + // Ask for transaction history - if it's empty then key has never been used + List historicTransactionHashes = this.getAddressTransactions(script, true); + + if (!historicTransactionHashes.isEmpty()) { + areAllKeysUnused = false; + this.blockchainCache.addKeyWithHistory(dKey); + } + } } if (areAllKeysUnused) { @@ -667,18 +696,25 @@ public abstract class Bitcoiny implements ForeignBlockchain { do { boolean areAllKeysUnused = true; - for (; ki < keys.size(); ++ki) { + for (; areAllKeysUnused && ki < keys.size(); ++ki) { DeterministicKey dKey = keys.get(ki); - // Check for transactions - Address address = Address.fromKey(this.params, dKey, ScriptType.P2PKH); - byte[] script = ScriptBuilder.createOutputScript(address).getProgram(); + // if the key already has a verified transaction history + if( this.blockchainCache.keyHasHistory(dKey)) { + areAllKeysUnused = false; + } + else { + // Check for transactions + Address address = Address.fromKey(this.params, dKey, ScriptType.P2PKH); + byte[] script = ScriptBuilder.createOutputScript(address).getProgram(); - // Ask for transaction history - if it's empty then key has never been used - List historicTransactionHashes = this.getAddressTransactions(script, false); + // Ask for transaction history - if it's empty then key has never been used + List historicTransactionHashes = this.getAddressTransactions(script, true); - if (!historicTransactionHashes.isEmpty()) { - areAllKeysUnused = false; + if (!historicTransactionHashes.isEmpty()) { + areAllKeysUnused = false; + this.blockchainCache.addKeyWithHistory(dKey); + } } } diff --git a/src/main/java/org/qortal/crosschain/BlockchainCache.java b/src/main/java/org/qortal/crosschain/BlockchainCache.java new file mode 100644 index 00000000..f6a1acf6 --- /dev/null +++ b/src/main/java/org/qortal/crosschain/BlockchainCache.java @@ -0,0 +1,89 @@ +package org.qortal.crosschain; + +import org.bitcoinj.crypto.DeterministicKey; +import org.qortal.settings.Settings; + +import java.util.Optional; +import java.util.Queue; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentLinkedDeque; + +/** + * Class BlockchainCache + * + * Cache blockchain information to reduce redundant RPCs to the ElectrumX servers. + */ +public class BlockchainCache { + + /** + * Keys With History + * + * Deterministic Keys with any transaction history. + */ + private Queue keysWithHistory = new ConcurrentLinkedDeque<>(); + + /** + * Transactions By Hash + * + * Transaction Hash -> Transaction + */ + private ConcurrentHashMap transactionByHash = new ConcurrentHashMap<>(); + + /** + * Cache Limit + * + * If this limit is reached, the cache will be cleared or reduced. + */ + private static final int CACHE_LIMIT = Settings.getInstance().getBlockchainCacheLimit(); + + /** + * Add Key With History + * + * @param key a deterministic key with a verified history + */ + public void addKeyWithHistory(DeterministicKey key) { + + if( this.keysWithHistory.size() > CACHE_LIMIT ) { + this.keysWithHistory.remove(); + } + + this.keysWithHistory.add(key); + } + + /** + * Key Has History? + * + * @param key the deterministic key + * + * @return true if the key has a history, otherwise false + */ + public boolean keyHasHistory( DeterministicKey key ) { + return this.keysWithHistory.contains(key); + } + + /** + * Add Transaction By Hash + * + * @param hash the transaction hash + * @param transaction the transaction + */ + public void addTransactionByHash( String hash, BitcoinyTransaction transaction ) { + + if( this.transactionByHash.size() > CACHE_LIMIT ) { + this.transactionByHash.clear(); + } + + this.transactionByHash.put(hash, transaction); + } + + /** + * Get Transaction By Hash + * + * @param hash the transaction hash + * + * @return the transaction, empty if the hash is not in the cache + */ + public Optional getTransactionByHash( String hash ) { + return Optional.ofNullable( this.transactionByHash.get(hash) ); + } +} \ No newline at end of file diff --git a/src/main/java/org/qortal/settings/Settings.java b/src/main/java/org/qortal/settings/Settings.java index de0ce5ed..f18ccd88 100644 --- a/src/main/java/org/qortal/settings/Settings.java +++ b/src/main/java/org/qortal/settings/Settings.java @@ -323,11 +323,14 @@ public class Settings { /* Foreign chains */ /** The number of consecutive empty addresses required before treating a wallet's transaction set as complete */ - private int gapLimit = 24; + private int gapLimit = 3; /** How many wallet keys to generate when using bitcoinj as the blockchain interface (e.g. when sending coins) */ private int bitcoinjLookaheadSize = 50; + /** How many units of data to be kept in a blockchain cache before the cache should be reduced or cleared. */ + private int blockchainCacheLimit = 1000; + // Data storage (QDN) /** Data storage enabled/disabled*/ @@ -1049,6 +1052,9 @@ public class Settings { return bitcoinjLookaheadSize; } + public int getBlockchainCacheLimit() { + return blockchainCacheLimit; + } public boolean isQdnEnabled() { return this.qdnEnabled;