Use cache for WalletService.getNumTxOutputsForAddress

Use a guava Multiset to cache the total number of tx outputs (out of the
live txs in the user's wallet) with a given address. Since this requires
a scan of the entire tx set, compute all the counts in one go and store
in an ImmutableMultiset<Address>. Invalidate the entire cache any time a
tx set change occurs, by attaching a WalletChangeEventListener to the
wallet (using a direct executor for immediate effect).

This is to fix a quadratic time bug in DepositView, which uses the count
to determine if a given address in the BTC wallet is used/unused.
This commit is contained in:
Steven Barclay 2021-01-26 15:32:16 +00:00
parent 70a13b8783
commit 217aaf826d
No known key found for this signature in database
GPG key ID: 9FED6BF1176D500B

View file

@ -72,6 +72,8 @@ import org.bitcoinj.wallet.listeners.WalletReorganizeEventListener;
import javax.inject.Inject; import javax.inject.Inject;
import com.google.common.collect.ImmutableMultiset;
import com.google.common.collect.Multiset;
import com.google.common.util.concurrent.FutureCallback; import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.MoreExecutors; import com.google.common.util.concurrent.MoreExecutors;
@ -83,8 +85,10 @@ import org.bouncycastle.crypto.params.KeyParameter;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Objects;
import java.util.Set; import java.util.Set;
import java.util.concurrent.CopyOnWriteArraySet; import java.util.concurrent.CopyOnWriteArraySet;
import java.util.concurrent.atomic.AtomicReference;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import lombok.Getter; import lombok.Getter;
@ -110,6 +114,8 @@ public abstract class WalletService {
private final CopyOnWriteArraySet<AddressConfidenceListener> addressConfidenceListeners = new CopyOnWriteArraySet<>(); private final CopyOnWriteArraySet<AddressConfidenceListener> addressConfidenceListeners = new CopyOnWriteArraySet<>();
private final CopyOnWriteArraySet<TxConfidenceListener> txConfidenceListeners = new CopyOnWriteArraySet<>(); private final CopyOnWriteArraySet<TxConfidenceListener> txConfidenceListeners = new CopyOnWriteArraySet<>();
private final CopyOnWriteArraySet<BalanceListener> balanceListeners = new CopyOnWriteArraySet<>(); private final CopyOnWriteArraySet<BalanceListener> balanceListeners = new CopyOnWriteArraySet<>();
private final WalletChangeEventListener cacheInvalidationListener;
private final AtomicReference<Multiset<Address>> txOutputAddressCache = new AtomicReference<>();
@Getter @Getter
protected Wallet wallet; protected Wallet wallet;
@Getter @Getter
@ -131,6 +137,8 @@ public abstract class WalletService {
this.feeService = feeService; this.feeService = feeService;
params = walletsSetup.getParams(); params = walletsSetup.getParams();
cacheInvalidationListener = wallet -> txOutputAddressCache.set(null);
} }
@ -143,6 +151,7 @@ public abstract class WalletService {
wallet.addCoinsSentEventListener(walletEventListener); wallet.addCoinsSentEventListener(walletEventListener);
wallet.addReorganizeEventListener(walletEventListener); wallet.addReorganizeEventListener(walletEventListener);
wallet.addTransactionConfidenceEventListener(walletEventListener); wallet.addTransactionConfidenceEventListener(walletEventListener);
wallet.addChangeEventListener(Threading.SAME_THREAD, cacheInvalidationListener);
} }
public void shutDown() { public void shutDown() {
@ -151,6 +160,7 @@ public abstract class WalletService {
wallet.removeCoinsSentEventListener(walletEventListener); wallet.removeCoinsSentEventListener(walletEventListener);
wallet.removeReorganizeEventListener(walletEventListener); wallet.removeReorganizeEventListener(walletEventListener);
wallet.removeTransactionConfidenceEventListener(walletEventListener); wallet.removeTransactionConfidenceEventListener(walletEventListener);
wallet.removeChangeEventListener(cacheInvalidationListener);
} }
} }
@ -496,16 +506,19 @@ public abstract class WalletService {
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////
public int getNumTxOutputsForAddress(Address address) { public int getNumTxOutputsForAddress(Address address) {
List<TransactionOutput> transactionOutputs = new ArrayList<>(); return getTxOutputAddressMultiset().count(address);
wallet.getTransactions(false).forEach(t -> transactionOutputs.addAll(t.getOutputs())); }
int outputs = 0;
for (TransactionOutput output : transactionOutputs) { private Multiset<Address> getTxOutputAddressMultiset() {
if (isOutputScriptConvertibleToAddress(output) && return txOutputAddressCache.updateAndGet(set -> set != null ? set : computeTxOutputAddressMultiset());
address != null && }
address.equals(getAddressFromOutput(output)))
outputs++; private Multiset<Address> computeTxOutputAddressMultiset() {
} return wallet.getTransactions(false).stream()
return outputs; .flatMap(t -> t.getOutputs().stream())
.map(WalletService::getAddressFromOutput)
.filter(Objects::nonNull)
.collect(ImmutableMultiset.toImmutableMultiset());
} }
public boolean isAddressUnused(Address address) { public boolean isAddressUnused(Address address) {