Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Foreign Wallet Balance Enhancements #202

Merged
merged 4 commits into from
Sep 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 52 additions & 16 deletions src/main/java/org/qortal/crosschain/Bitcoiny.java
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -509,8 +516,22 @@ public List<SimpleTransaction> getWalletTransactions(String key58) throws Foreig
if (!historicTransactionHashes.isEmpty()) {
areAllKeysUnused = false;

for (TransactionHash transactionHash : historicTransactionHashes)
walletTransactions.add(this.getTransaction(transactionHash.txHash));
for (TransactionHash transactionHash : historicTransactionHashes) {

Optional<BitcoinyTransaction> 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);
}
}
}
}

Expand Down Expand Up @@ -602,17 +623,25 @@ public Set<String> getWalletAddresses(String key58) throws ForeignBlockchainExce
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.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<TransactionHash> historicTransactionHashes = this.getAddressTransactions(script, true);

if (!historicTransactionHashes.isEmpty()) {
areAllKeysUnused = false;
this.blockchainCache.addKeyWithHistory(dKey);
}
}
}

if (areAllKeysUnused) {
Expand Down Expand Up @@ -667,18 +696,25 @@ private List<DeterministicKey> getOldWalletKeys(String masterPrivateKey) throws
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<TransactionHash> historicTransactionHashes = this.getAddressTransactions(script, false);
// Ask for transaction history - if it's empty then key has never been used
List<TransactionHash> historicTransactionHashes = this.getAddressTransactions(script, true);

if (!historicTransactionHashes.isEmpty()) {
areAllKeysUnused = false;
if (!historicTransactionHashes.isEmpty()) {
areAllKeysUnused = false;
this.blockchainCache.addKeyWithHistory(dKey);
}
}
}

Expand Down
89 changes: 89 additions & 0 deletions src/main/java/org/qortal/crosschain/BlockchainCache.java
Original file line number Diff line number Diff line change
@@ -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<DeterministicKey> keysWithHistory = new ConcurrentLinkedDeque<>();

/**
* Transactions By Hash
*
* Transaction Hash -> Transaction
*/
private ConcurrentHashMap<String, BitcoinyTransaction> 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<BitcoinyTransaction> getTransactionByHash( String hash ) {
return Optional.ofNullable( this.transactionByHash.get(hash) );
}
}
8 changes: 7 additions & 1 deletion src/main/java/org/qortal/settings/Settings.java
Original file line number Diff line number Diff line change
Expand Up @@ -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*/
Expand Down Expand Up @@ -1049,6 +1052,9 @@ public int getBitcoinjLookaheadSize() {
return bitcoinjLookaheadSize;
}

public int getBlockchainCacheLimit() {
return blockchainCacheLimit;
}

public boolean isQdnEnabled() {
return this.qdnEnabled;
Expand Down
Loading