Skip to content

Commit

Permalink
cache fetched script hash status and dont retrieve history again unti…
Browse files Browse the repository at this point in the history
…l it changes
  • Loading branch information
craigraw committed Apr 29, 2021
1 parent 5518116 commit d6c7a0b
Show file tree
Hide file tree
Showing 4 changed files with 57 additions and 9 deletions.
1 change: 1 addition & 0 deletions src/main/java/com/sparrowwallet/sparrow/AppController.java
Expand Up @@ -1010,6 +1010,7 @@ public void refreshWallet(ActionEvent event) {
Wallet pastWallet = wallet.copy();
walletTabData.getStorage().backupTempWallet();
wallet.clearHistory();
AppServices.clearTransactionHistoryCache(wallet);
EventManager.get().post(new WalletAddressesChangedEvent(wallet, pastWallet, walletTabData.getStorage().getWalletFile()));
}
}
Expand Down
4 changes: 4 additions & 0 deletions src/main/java/com/sparrowwallet/sparrow/AppServices.java
Expand Up @@ -496,6 +496,10 @@ public static void addPayjoinURI(BitcoinURI bitcoinURI) {
payjoinURIs.put(bitcoinURI.getAddress(), bitcoinURI);
}

public static void clearTransactionHistoryCache(Wallet wallet) {
ElectrumServer.clearRetrievedScriptHashes(wallet);
}

public static Optional<ButtonType> showWarningDialog(String title, String content, ButtonType... buttons) {
return showAlertDialog(title, content, Alert.AlertType.WARNING, buttons);
}
Expand Down
60 changes: 51 additions & 9 deletions src/main/java/com/sparrowwallet/sparrow/net/ElectrumServer.java
Expand Up @@ -44,6 +44,10 @@ public class ElectrumServer {

private static final Map<String, Set<String>> subscribedScriptHashes = Collections.synchronizedMap(new HashMap<>());

private static String previousServerAddress;

private static Map<String, String> retrievedScriptHashes = Collections.synchronizedMap(new HashMap<>());

private static ElectrumServerRpc electrumServerRpc = new SimpleElectrumServerRpc();

private static String bwtElectrumServer;
Expand Down Expand Up @@ -84,6 +88,12 @@ private static synchronized Transport getTransport() throws ServerException {
throw new ServerConfigException("Electrum server URL must start with " + Protocol.TCP.toUrlString() + " or " + Protocol.SSL.toUrlString());
}

//If changing server, don't rely on previous transaction history
if(!electrumServer.equals(previousServerAddress)) {
retrievedScriptHashes.clear();
}
previousServerAddress = electrumServer;

HostAndPort server = protocol.getServerHostAndPort(electrumServer);

if(Config.get().isUseProxy() && proxyServer != null && !proxyServer.isBlank()) {
Expand Down Expand Up @@ -150,6 +160,11 @@ public static synchronized void closeActiveConnection() throws ServerException {
}
}

public static void clearRetrievedScriptHashes(Wallet wallet) {
wallet.getNode(KeyPurpose.RECEIVE).getChildren().stream().map(node -> getScriptHash(wallet, node)).forEach(scriptHash -> retrievedScriptHashes.remove(scriptHash));
wallet.getNode(KeyPurpose.CHANGE).getChildren().stream().map(node -> getScriptHash(wallet, node)).forEach(scriptHash -> retrievedScriptHashes.remove(scriptHash));
}

public Map<WalletNode, Set<BlockTransactionHash>> getHistory(Wallet wallet) throws ServerException {
Map<WalletNode, Set<BlockTransactionHash>> receiveTransactionMap = new TreeMap<>();
getHistory(wallet, KeyPurpose.RECEIVE, receiveTransactionMap);
Expand Down Expand Up @@ -177,6 +192,15 @@ public Map<WalletNode, Set<BlockTransactionHash>> getHistory(Wallet wallet, Coll
Set<BlockTransactionHash> newReferences = nodeTransactionMap.values().stream().flatMap(Collection::stream).filter(ref -> !wallet.getTransactions().containsKey(ref.getHash())).collect(Collectors.toSet());
getReferencedTransactions(wallet, nodeTransactionMap);

//Subscribe and retrieve transaction history from child nodes if necessary to maintain gap limit
Set<KeyPurpose> keyPurposes = nodes.stream().map(WalletNode::getKeyPurpose).collect(Collectors.toUnmodifiableSet());
for(KeyPurpose keyPurpose : keyPurposes) {
WalletNode purposeNode = wallet.getNode(keyPurpose);
getHistoryToGapLimit(wallet, nodeTransactionMap, purposeNode);
}

log.debug("Fetched nodes history for: " + nodeTransactionMap.keySet());

if(!newReferences.isEmpty()) {
//Look for additional nodes to fetch history for by considering the inputs and outputs of new transactions found
log.debug(wallet.getName() + " found new transactions: " + newReferences);
Expand Down Expand Up @@ -216,13 +240,22 @@ public Map<WalletNode, Set<BlockTransactionHash>> getHistory(Wallet wallet, Coll

public void getHistory(Wallet wallet, KeyPurpose keyPurpose, Map<WalletNode, Set<BlockTransactionHash>> nodeTransactionMap) throws ServerException {
WalletNode purposeNode = wallet.getNode(keyPurpose);
//Subscribe to all existing address WalletNodes and add them to nodeTransactionMap as keys to empty sets if they have history
//Subscribe to all existing address WalletNodes and add them to nodeTransactionMap as keys to empty sets if they have history that needs to be fetched
subscribeWalletNodes(wallet, purposeNode.getChildren(), nodeTransactionMap, 0);
//All WalletNode keys in nodeTransactionMap need to have their history fetched (nodes without history will not be keys in the map yet)
getReferences(wallet, nodeTransactionMap.keySet(), nodeTransactionMap, 0);
//Fetch all referenced transaction to wallet transactions map. We do this now even though it is done again later to get it done before too many script hashes are subscribed
getReferencedTransactions(wallet, nodeTransactionMap);
//Increase child nodes if necessary to maintain gap limit, and ensure they are subscribed and history is fetched
getHistoryToGapLimit(wallet, nodeTransactionMap, purposeNode);

log.debug("Fetched history for: " + nodeTransactionMap.keySet());

//Set the remaining WalletNode keys in nodeTransactionMap to empty sets to indicate no history (if no script hash history has already been retrieved in a previous call)
purposeNode.getChildren().stream().filter(node -> !nodeTransactionMap.containsKey(node) && retrievedScriptHashes.get(getScriptHash(wallet, node)) == null).forEach(node -> nodeTransactionMap.put(node, Collections.emptySet()));
}

private void getHistoryToGapLimit(Wallet wallet, Map<WalletNode, Set<BlockTransactionHash>> nodeTransactionMap, WalletNode purposeNode) throws ServerException {
//Because node children are added sequentially in WalletNode.fillToIndex, we can simply look at the number of children to determine the highest filled index
int historySize = purposeNode.getChildren().size();
//The gap limit size takes the highest used index in the retrieved history and adds the gap limit (plus one to be comparable to the number of children since index is zero based)
Expand All @@ -235,9 +268,6 @@ public void getHistory(Wallet wallet, KeyPurpose keyPurpose, Map<WalletNode, Set
historySize = purposeNode.getChildren().size();
gapLimitSize = getGapLimitSize(wallet, nodeTransactionMap);
}

//Set the remaining WalletNode keys in nodeTransactionMap to empty sets to indicate no history
purposeNode.getChildren().stream().filter(node -> !nodeTransactionMap.containsKey(node)).forEach(node -> nodeTransactionMap.put(node, Collections.emptySet()));
}

private int getGapLimitSize(Wallet wallet, Map<WalletNode, Set<BlockTransactionHash>> nodeTransactionMap) {
Expand Down Expand Up @@ -319,16 +349,21 @@ public void subscribeWalletNodes(Wallet wallet, Collection<WalletNode> nodes, Ma

if(node != null && node.getIndex() >= startIndex) {
String scriptHash = getScriptHash(wallet, node);
if(getSubscribedScriptHashStatus(scriptHash) != null) {
//Already subscribed, but still need to fetch history from a used node
nodeTransactionMap.put(node, new TreeSet<>());
String subscribedStatus = getSubscribedScriptHashStatus(scriptHash);
if(subscribedStatus != null) {
//Already subscribed, but still need to fetch history from a used node if not previously fetched
if(!subscribedStatus.equals(retrievedScriptHashes.get(scriptHash))) {
nodeTransactionMap.put(node, new TreeSet<>());
}
} else if(!subscribedScriptHashes.containsKey(scriptHash) && scriptHashes.add(scriptHash)) {
//Unique script hash we are not yet subscribed to
pathScriptHashes.put(node.getDerivationPath(), scriptHash);
}
}
}

log.debug("Subscribe to: " + pathScriptHashes.keySet());

if(pathScriptHashes.isEmpty()) {
return;
}
Expand All @@ -343,8 +378,8 @@ public void subscribeWalletNodes(Wallet wallet, Collection<WalletNode> nodes, Ma
WalletNode node = optionalNode.get();
String scriptHash = getScriptHash(wallet, node);

//Check if there is history for this script hash
if(status != null) {
//Check if there is history for this script hash, and if the history has changed since last fetched
if(status != null && !status.equals(retrievedScriptHashes.get(scriptHash))) {
//Set the value for this node to be an empty set to mark it as requiring a get_history RPC call for this wallet
nodeTransactionMap.put(node, new TreeSet<>());
}
Expand Down Expand Up @@ -1037,6 +1072,13 @@ protected Boolean call() throws ServerException {
Map<WalletNode, Set<BlockTransactionHash>> nodeTransactionMap = (nodes == null ? electrumServer.getHistory(wallet) : electrumServer.getHistory(wallet, nodes));
electrumServer.getReferencedTransactions(wallet, nodeTransactionMap);
electrumServer.calculateNodeHistory(wallet, nodeTransactionMap);

//Add all of the script hashes we have now fetched the history for so we don't need to fetch again until the script hash status changes
for(WalletNode node : nodeTransactionMap.keySet()) {
String scriptHash = getScriptHash(wallet, node);
retrievedScriptHashes.put(scriptHash, getSubscribedScriptHashStatus(scriptHash));
}

return true;
}

Expand Down
Expand Up @@ -318,6 +318,7 @@ public void walletNodeHistoryChanged(WalletNodeHistoryChangedEvent event) {
public void walletTabsClosed(WalletTabsClosedEvent event) {
for(WalletTabData tabData : event.getClosedWalletTabData()) {
if(tabData.getWalletForm() == this) {
AppServices.clearTransactionHistoryCache(wallet);
EventManager.get().unregister(this);
}
}
Expand Down

0 comments on commit d6c7a0b

Please sign in to comment.